fix: 修复 FCStd 导入后变换异常

dev
Zhaowenlong 2 days ago
parent 0bd5ccb2ad
commit 58c9eae33c

@ -6,6 +6,7 @@ import FreeCAD as App
import FreeCADGui as Gui import FreeCADGui as Gui
import ImportGui import ImportGui
import DevicePreview import DevicePreview
import TemplateSemantics
import TerminalObjects import TerminalObjects
@ -54,6 +55,17 @@ def _safe_token(value):
return "".join(chars) return "".join(chars)
def _unique_object_name(doc, base_name):
base = _safe_token(base_name) or "QETObject"
if doc.getObject(base) is None:
return base
suffix = 1
while doc.getObject("{0}_{1}".format(base, suffix)) is not None:
suffix += 1
return "{0}_{1}".format(base, suffix)
def _native_path(value): def _native_path(value):
text = (value or "").strip() text = (value or "").strip()
if not text: if not text:
@ -156,9 +168,34 @@ def _ensure_document(scene_path):
preferred_name = _safe_token(Path(scene_path).stem if scene_path else "QETScene")[:48] or "QETScene" preferred_name = _safe_token(Path(scene_path).stem if scene_path else "QETScene")[:48] or "QETScene"
existing_doc = DevicePreview.find_main_exchange_document(preferred_name) existing_doc = DevicePreview.find_main_exchange_document(preferred_name)
if existing_doc is not None: if existing_doc is not None:
_activate_document(existing_doc)
return existing_doc return existing_doc
return App.newDocument(preferred_name) doc = App.newDocument(preferred_name)
_activate_document(doc)
return doc
def _activate_document(doc):
if doc is None:
return
setter = getattr(App, "setActiveDocument", None)
if callable(setter):
try:
setter(doc.Name)
except Exception:
pass
try:
App.ActiveDocument = doc
except Exception:
pass
try:
Gui.ActiveDocument = Gui.getDocument(doc.Name)
except Exception:
pass
def _cabinet_label_text(cabinet): def _cabinet_label_text(cabinet):
@ -387,12 +424,103 @@ def _remove_object_tree(doc, obj):
if obj is None: if obj is None:
return return
obj_name = _object_name(obj)
if not obj_name or doc.getObject(obj_name) is None:
_detach_from_parent_groups(obj)
return
children = list(getattr(obj, "Group", []) or []) children = list(getattr(obj, "Group", []) or [])
for child in children: for child in children:
_remove_object_tree(doc, child) _remove_object_tree(doc, child)
if doc.getObject(obj.Name) is not None: _detach_from_parent_groups(obj)
doc.removeObject(obj.Name) if doc.getObject(obj_name) is not None:
doc.removeObject(obj_name)
def _object_name(obj):
try:
return getattr(obj, "Name", "")
except Exception:
return ""
def _object_exists(doc, obj):
obj_name = _object_name(obj)
return bool(obj_name and doc.getObject(obj_name) is not None)
def _detach_from_parent_groups(obj):
try:
parents = list(getattr(obj, "InList", []) or [])
except Exception:
return
for parent in parents:
try:
group_children = list(getattr(parent, "Group", []) or [])
except Exception:
continue
if obj not in group_children:
continue
remover = getattr(parent, "removeObject", None)
if callable(remover):
try:
remover(obj)
continue
except Exception:
pass
try:
while obj in getattr(parent, "Group", []):
parent.Group.remove(obj)
except Exception:
pass
def _linked_document_objects(value):
if value is None:
return []
if hasattr(value, "Name") and hasattr(value, "TypeId"):
return [value]
if isinstance(value, dict):
result = []
for item in value.values():
result.extend(_linked_document_objects(item))
return result
if isinstance(value, (list, tuple, set)):
result = []
for item in value:
result.extend(_linked_document_objects(item))
return result
return []
def _remove_template_terminal_hint_object(doc, obj):
linked_objects = []
for prop_name in ("OriginFeatures",):
linked_objects.extend(_linked_document_objects(getattr(obj, prop_name, None)))
_remove_object_tree(doc, obj)
for linked_obj in linked_objects:
if linked_obj is not obj:
_remove_object_tree(doc, linked_obj)
def _remove_template_terminal_hints(doc, container):
removed = 0
if container is None:
return removed
for child in list(getattr(container, "Group", []) or []):
if TerminalObjects.is_template_terminal_object(child):
_remove_template_terminal_hint_object(doc, child)
removed += 1
continue
if hasattr(child, "Group"):
removed += _remove_template_terminal_hints(doc, child)
return removed
def _clear_group_contents(doc, group): def _clear_group_contents(doc, group):
@ -405,6 +533,126 @@ def _clear_group_contents(doc, group):
_remove_object_tree(doc, child) _remove_object_tree(doc, child)
def _is_exchange_sidecar_group(obj):
child_name = _object_name(obj)
if child_name.startswith(TERMINAL_GROUP_PREFIX) or child_name.startswith(WIRE_GROUP_PREFIX):
return True
return getattr(obj, "QetGroupKind", "").strip() in {GROUP_KIND_TERMINALS, GROUP_KIND_WIRES}
def _keep_only_direct_model_children(device_group, direct_model_objects):
allowed_ids = {id(obj) for obj in direct_model_objects if obj is not None}
kept_children = []
for child in list(getattr(device_group, "Group", []) or []):
if id(child) in allowed_ids:
kept_children.append(child)
continue
if _is_exchange_sidecar_group(child):
kept_children.append(child)
try:
device_group.Group = kept_children
except Exception:
try:
device_group.Group[:] = kept_children
except Exception:
pass
def _document_object_links(obj):
linked_objects = []
for prop_name in ("Links", "OutList"):
linked_objects.extend(_linked_document_objects(getattr(obj, prop_name, None)))
return linked_objects
def _shape_copy(shape):
copier = getattr(shape, "copy", None)
if callable(copier):
try:
return copier()
except Exception:
pass
return shape
def _can_materialize_shape(obj):
if getattr(obj, "TypeId", "") == "Part::Feature":
return False
try:
shape = getattr(obj, "Shape", None)
except Exception:
return False
return shape is not None
def _materialize_shape_object(doc, source_obj):
source_name = _object_name(source_obj) or "Model"
target = doc.addObject(
"Part::Feature",
_unique_object_name(doc, source_name + "_Shape"),
)
target.Label = getattr(source_obj, "Label", source_name)
target.Shape = _shape_copy(getattr(source_obj, "Shape", None))
try:
target.Placement = getattr(source_obj, "Placement")
except Exception:
pass
try:
target.ViewObject.Visibility = getattr(source_obj.ViewObject, "Visibility", True)
except Exception:
pass
try:
target.ViewObject.ShapeColor = getattr(source_obj.ViewObject, "ShapeColor")
except Exception:
pass
return target
def _materialize_direct_model_objects(doc, device_group, model_objects):
direct_objects = []
obsolete_objects = []
materialized_labels = []
for obj in model_objects:
if not _object_exists(doc, obj):
continue
if not _can_materialize_shape(obj):
direct_objects.append(obj)
continue
static_obj = _materialize_shape_object(doc, obj)
if static_obj not in getattr(device_group, "Group", []):
device_group.addObject(static_obj)
direct_objects.append(static_obj)
materialized_labels.append((static_obj, getattr(obj, "Label", _object_name(obj))))
obsolete_objects.append(obj)
obsolete_objects.extend(_document_object_links(obj))
direct_names = {_object_name(obj) for obj in direct_objects}
removed_names = set()
for obsolete in obsolete_objects:
obsolete_name = _object_name(obsolete)
if not obsolete_name or obsolete_name in direct_names or obsolete_name in removed_names:
continue
_remove_object_tree(doc, obsolete)
removed_names.add(obsolete_name)
for obj, label in materialized_labels:
try:
obj.Label = label
except Exception:
pass
return direct_objects
def _generate_instance_id(project_uuid, element_uuid): def _generate_instance_id(project_uuid, element_uuid):
seed = "QET:{0}:{1}".format((project_uuid or "").strip(), (element_uuid or "").strip()) seed = "QET:{0}:{1}".format((project_uuid or "").strip(), (element_uuid or "").strip())
return str(uuid.uuid5(uuid.NAMESPACE_URL, seed)) return str(uuid.uuid5(uuid.NAMESPACE_URL, seed))
@ -445,6 +693,7 @@ def _import_model_into_group(doc, device_group, model_path, merge=False, use_lin
for obj in top_level_objects: for obj in top_level_objects:
if obj not in getattr(device_group, "Group", []): if obj not in getattr(device_group, "Group", []):
device_group.addObject(obj) device_group.addObject(obj)
TemplateSemantics.clear_stored_template_slot_hints(device_group)
TerminalObjects.hide_template_terminal_hints(device_group) TerminalObjects.hide_template_terminal_hints(device_group)
return top_level_objects return top_level_objects
@ -467,6 +716,7 @@ def _import_fcstd_into_group(doc, device_group, model_path):
if source_doc is None: if source_doc is None:
raise DeviceImportError("Cannot open FCStd file") raise DeviceImportError("Cannot open FCStd file")
TemplateSemantics.clear_stored_template_slot_hints(device_group)
top_level_objects = _top_level_document_objects(source_doc) top_level_objects = _top_level_document_objects(source_doc)
copied_objects = [] copied_objects = []
for source_obj in top_level_objects: for source_obj in top_level_objects:
@ -475,14 +725,28 @@ def _import_fcstd_into_group(doc, device_group, model_path):
device_group.addObject(copied_obj) device_group.addObject(copied_obj)
copied_objects.append(copied_obj) copied_objects.append(copied_obj)
TerminalObjects.hide_template_terminal_hints(device_group) template_slots = TemplateSemantics.collect_live_terminal_hints(device_group)
return copied_objects TemplateSemantics.store_template_slot_hints(device_group, template_slots)
_remove_template_terminal_hints(doc, device_group)
copied_model_objects = [
obj
for obj in copied_objects
if _object_exists(doc, obj)
]
direct_model_objects = _materialize_direct_model_objects(
doc,
device_group,
copied_model_objects,
)
_keep_only_direct_model_children(device_group, direct_model_objects)
return direct_model_objects
finally: finally:
if should_close and source_doc is not None: if should_close and source_doc is not None:
try: try:
App.closeDocument(source_doc.Name) App.closeDocument(source_doc.Name)
except Exception: except Exception:
pass pass
_activate_document(doc)
def _model_index(payload): def _model_index(payload):

@ -8,6 +8,9 @@ import FreeCAD as App
import TerminalObjects as TerminalObjects import TerminalObjects as TerminalObjects
STORED_TEMPLATE_SLOTS_PROPERTY = "QetTemplateSlotsJson"
def _sidecar_candidates(model_path): def _sidecar_candidates(model_path):
native = TerminalObjects.native_path(model_path) native = TerminalObjects.native_path(model_path)
if not native: if not native:
@ -113,6 +116,111 @@ def _rotation_from_object(source_object):
} }
def _vector_to_payload(value):
vector = _vector_from_value(value)
if vector is None:
return None
return [float(vector.x), float(vector.y), float(vector.z)]
def _rotation_to_payload(value):
if not isinstance(value, dict):
return None
axis = _vector_to_payload(value.get("axis"))
angle = value.get("angle")
if axis is None or angle is None:
return None
try:
angle = float(angle)
except (TypeError, ValueError):
return None
return {
"axis": axis,
"angle": angle,
}
def _slot_to_payload(slot):
if not isinstance(slot, dict):
return None
base = _vector_to_payload(slot.get("base"))
if base is None:
return None
name = (slot.get("name") or slot.get("label") or "").strip()
label = (slot.get("label") or name).strip()
payload = {
"name": name,
"label": label,
"base": base,
}
rotation = _rotation_to_payload(slot.get("rotation"))
if rotation is not None:
payload["rotation"] = rotation
return payload
def _stored_template_slot_payloads(container):
raw_text = getattr(container, STORED_TEMPLATE_SLOTS_PROPERTY, "") if container is not None else ""
if not isinstance(raw_text, str) or not raw_text.strip():
return []
try:
payload = json.loads(raw_text)
except (TypeError, ValueError):
return []
if isinstance(payload, dict):
entries = payload.get("terminal_slots")
elif isinstance(payload, list):
entries = payload
else:
entries = None
if not isinstance(entries, list):
return []
return entries
def clear_stored_template_slot_hints(container):
if container is None:
return
if STORED_TEMPLATE_SLOTS_PROPERTY in getattr(container, "PropertiesList", []):
setattr(container, STORED_TEMPLATE_SLOTS_PROPERTY, "")
def store_template_slot_hints(container, slots):
if container is None:
return 0
payloads = []
for slot in slots or []:
payload = _slot_to_payload(slot)
if payload is not None:
payloads.append(payload)
raw_text = ""
if payloads:
raw_text = json.dumps(
{"terminal_slots": payloads},
ensure_ascii=False,
sort_keys=True,
)
TerminalObjects.ensure_string_property(
container,
STORED_TEMPLATE_SLOTS_PROPERTY,
"QET Exchange",
"Serialized FCStd template terminal slots",
raw_text,
)
return len(payloads)
def _slot_from_payload(item, source, index, source_object=None): def _slot_from_payload(item, source, index, source_object=None):
if not isinstance(item, dict): if not isinstance(item, dict):
return None return None
@ -179,11 +287,23 @@ def _is_child_group(obj, group_kind):
return False return False
def collect_terminal_hints(container): def _collect_stored_terminal_hints(container):
hints = []
for index, item in enumerate(_stored_template_slot_payloads(container)):
hint = _slot_from_payload(item, "model", index, source_object=None)
if hint is not None:
hints.append(hint)
return hints
def _collect_terminal_hints(container, include_stored):
hints = [] hints = []
if container is None: if container is None:
return hints return hints
if include_stored:
hints.extend(_collect_stored_terminal_hints(container))
for child in list(getattr(container, "Group", []) or []): for child in list(getattr(container, "Group", []) or []):
if TerminalObjects.is_terminal_hint_object(child) and not TerminalObjects.is_terminal_object(child): if TerminalObjects.is_terminal_hint_object(child) and not TerminalObjects.is_terminal_object(child):
slot_name = ( slot_name = (
@ -214,12 +334,20 @@ def collect_terminal_hints(container):
continue continue
if hasattr(child, "Group"): if hasattr(child, "Group"):
hints.extend(collect_terminal_hints(child)) hints.extend(_collect_terminal_hints(child, include_stored))
hints.sort(key=lambda item: (item.get("label", ""), item.get("name", ""))) hints.sort(key=lambda item: (item.get("label", ""), item.get("name", "")))
return hints return hints
def collect_terminal_hints(container):
return _collect_terminal_hints(container, include_stored=True)
def collect_live_terminal_hints(container):
return _collect_terminal_hints(container, include_stored=False)
def collect_model_bounding_box(container): def collect_model_bounding_box(container):
boxes = [] boxes = []

@ -0,0 +1,482 @@
import importlib
import sys
import types
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange"
if str(MODULE_DIR) not in sys.path:
sys.path.insert(0, str(MODULE_DIR))
def _install_fake_freecad(source_doc):
class Vector:
def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = float(x)
self.y = float(y)
self.z = float(z)
class Rotation:
def __init__(self, axis=None, angle=None):
self.Axis = axis
self.Angle = angle
class Placement:
def __init__(self, base=None, rotation=None):
self.Base = base or Vector()
self.Rotation = rotation or Rotation()
fake_freecad = types.ModuleType("FreeCAD")
fake_freecad.Vector = Vector
fake_freecad.Rotation = Rotation
fake_freecad.Placement = Placement
fake_freecad.Console = types.SimpleNamespace(
PrintMessage=lambda *args, **kwargs: None,
PrintWarning=lambda *args, **kwargs: None,
PrintError=lambda *args, **kwargs: None,
)
fake_freecad.ActiveDocument = None
fake_freecad.set_active_document_calls = []
def set_active_document(name):
fake_freecad.set_active_document_calls.append(name)
def close_document(*args, **kwargs):
fake_freecad.ActiveDocument = None
fake_freecad.setActiveDocument = set_active_document
fake_freecad.listDocuments = lambda: {}
fake_freecad.openDocument = lambda *args, **kwargs: source_doc
fake_freecad.closeDocument = close_document
sys.modules["FreeCAD"] = fake_freecad
fake_freecadgui = types.ModuleType("FreeCADGui")
fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None
fake_freecadgui.addCommand = lambda *args, **kwargs: None
fake_freecadgui.getDocument = lambda *args, **kwargs: object()
fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: [])
sys.modules["FreeCADGui"] = fake_freecadgui
fake_importgui = types.ModuleType("ImportGui")
fake_importgui.insert = lambda *args, **kwargs: None
sys.modules["ImportGui"] = fake_importgui
fake_device_preview = types.ModuleType("DevicePreview")
fake_device_preview.find_main_exchange_document = lambda *args, **kwargs: None
sys.modules["DevicePreview"] = fake_device_preview
class FakeViewObject:
def __init__(self):
self.Visibility = True
self.ShapeColor = None
class FakeShape:
def copy(self):
return FakeShape()
class FakeObject:
def __init__(self, name, type_id):
self.Name = name
self.Label = name
self.TypeId = type_id
self._deleted = False
self._raise_name_when_deleted = False
self._raise_group_when_deleted = False
self._raise_inlist_when_deleted = False
self.PropertiesList = []
self.Group = []
self.InList = []
self.Links = []
self.ViewObject = FakeViewObject()
self.Placement = sys.modules["FreeCAD"].Placement()
self.Shape = FakeShape()
def __getattribute__(self, name):
if name == "Name":
data = object.__getattribute__(self, "__dict__")
if data.get("_deleted") and data.get("_raise_name_when_deleted"):
raise RuntimeError("Cannot access attribute 'Name' of deleted object")
if name == "Group":
data = object.__getattribute__(self, "__dict__")
if data.get("_deleted") and data.get("_raise_group_when_deleted"):
raise RuntimeError("Cannot access attribute 'Group' of deleted object")
if name == "InList":
data = object.__getattribute__(self, "__dict__")
if data.get("_deleted") and data.get("_raise_inlist_when_deleted"):
raise RuntimeError("Cannot access attribute 'InList' of deleted object")
return object.__getattribute__(self, name)
def isDerivedFrom(self, type_name):
if self.TypeId == type_name:
return True
if type_name == "App::DocumentObjectGroup":
return self.TypeId in {"App::DocumentObjectGroup", "App::Part"}
if type_name == "App::LocalCoordinateSystem":
return self.TypeId in {"Part::LocalCoordinateSystem", "PartDesign::CoordinateSystem"}
return False
def addProperty(self, prop_type, prop_name, group_name, description):
if prop_name not in self.PropertiesList:
self.PropertiesList.append(prop_name)
def addObject(self, child):
if child not in self.Group:
self.Group.append(child)
if self not in child.InList:
child.InList.append(self)
for dependency in list(getattr(child, "Links", []) or []):
if dependency not in self.Group:
self.Group.append(dependency)
if self not in dependency.InList:
dependency.InList.append(self)
class FakeDocument:
def __init__(
self,
name,
filename="",
detach_on_remove=True,
raise_name_when_deleted=False,
raise_group_when_deleted=False,
raise_inlist_when_deleted=False,
):
self.Name = name
self.FileName = filename
self.Objects = []
self.detach_on_remove = detach_on_remove
self.raise_name_when_deleted = raise_name_when_deleted
self.raise_group_when_deleted = raise_group_when_deleted
self.raise_inlist_when_deleted = raise_inlist_when_deleted
def addObject(self, type_name, name):
obj = FakeObject(name, type_name)
obj._raise_name_when_deleted = self.raise_name_when_deleted
obj._raise_group_when_deleted = self.raise_group_when_deleted
obj._raise_inlist_when_deleted = self.raise_inlist_when_deleted
self.Objects.append(obj)
return obj
def getObject(self, name):
for obj in self.Objects:
if obj.Name == name:
return obj
return None
def removeObject(self, name):
target = self.getObject(name)
if target is None:
return
self.Objects = [obj for obj in self.Objects if obj is not target]
target._deleted = True
if self.detach_on_remove:
for obj in self.Objects:
if target in getattr(obj, "Group", []):
obj.Group.remove(target)
if target in getattr(obj, "InList", []):
obj.InList.remove(target)
def copyObject(self, source_obj, recursive):
copies = {}
def clone(obj):
if obj in copies:
return copies[obj]
copied = FakeObject(obj.Name, obj.TypeId)
copied._raise_name_when_deleted = self.raise_name_when_deleted
copied._raise_group_when_deleted = self.raise_group_when_deleted
copied._raise_inlist_when_deleted = self.raise_inlist_when_deleted
copied.Label = obj.Label
copied.PropertiesList = list(getattr(obj, "PropertiesList", []) or [])
copied.Placement = obj.Placement
for prop in copied.PropertiesList:
setattr(copied, prop, getattr(obj, prop, None))
self.Objects.append(copied)
copies[obj] = copied
if recursive:
copied.Links = [
clone(child)
for child in list(getattr(obj, "Links", []) or [])
]
for child in copied.Links:
if copied not in child.InList:
child.InList.append(copied)
for child in list(getattr(obj, "Group", []) or []):
copied.addObject(clone(child))
copied.OriginFeatures = [
clone(child)
for child in list(getattr(obj, "OriginFeatures", []) or [])
]
return copied
return clone(source_obj)
def _reload_modules():
for name in [
"DeviceImport",
"TemplateSemantics",
"TerminalObjects",
]:
sys.modules.pop(name, None)
device_import = importlib.import_module("DeviceImport")
template_semantics = importlib.import_module("TemplateSemantics")
return device_import, template_semantics
class FcstdDeviceImportTest(unittest.TestCase):
def test_fcstd_import_preserves_template_slots_without_live_template_lcs(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
app = sys.modules["FreeCAD"]
body = source.addObject("Part::Feature", "Body")
terminal = source.addObject("Part::LocalCoordinateSystem", "Terminal_D1")
terminal.Label = "D1"
terminal.Placement = app.Placement(
app.Vector(11, 22, 33),
app.Rotation(app.Vector(0, 0, 1), 90),
)
terminal.addProperty("App::PropertyString", "Role", "QET Template", "")
terminal.Role = "Terminal"
terminal.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
terminal.QetTemplateSlotName = "D1"
x_axis = source.addObject("App::Line", "Terminal_D1_XAxis")
terminal.OriginFeatures = [x_axis]
terminal.addObject(x_axis)
doc = FakeDocument("QETScene")
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, template_semantics = _reload_modules()
copied_objects = device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertEqual([body.Name], [obj.Name for obj in copied_objects])
self.assertEqual(["Body"], [obj.Name for obj in device_group.Group])
self.assertNotIn("Terminal_D1", [obj.Name for obj in doc.Objects])
self.assertNotIn("Terminal_D1_XAxis", [obj.Name for obj in doc.Objects])
self.assertIn("QetTemplateSlotsJson", device_group.PropertiesList)
slots = template_semantics.collect_terminal_hints(device_group)
self.assertEqual(1, len(slots))
self.assertEqual("D1", slots[0]["name"])
self.assertEqual(11.0, slots[0]["base"].x)
self.assertEqual(90.0, slots[0]["rotation"]["angle"])
self.assertIsNone(slots[0]["source_object"])
def test_fcstd_import_keeps_link_dependencies_out_of_device_group(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
solid = source.addObject("Part::Feature", "Solid")
solid_001 = source.addObject("Part::Feature", "Solid001")
compound = source.addObject("Part::Compound2", "Compound")
compound.Links = [solid, solid_001]
solid.InList.append(compound)
solid_001.InList.append(compound)
doc = FakeDocument("QETScene")
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, _ = _reload_modules()
copied_objects = device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertEqual(1, len(copied_objects))
self.assertEqual("Part::Feature", copied_objects[0].TypeId)
self.assertEqual("Compound", copied_objects[0].Label)
self.assertEqual(copied_objects, device_group.Group)
self.assertEqual([], copied_objects[0].Links)
self.assertIsNone(doc.getObject("Compound"))
self.assertIsNone(doc.getObject("Solid"))
self.assertIsNone(doc.getObject("Solid001"))
def test_fcstd_import_restores_target_document_after_closing_source_doc(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
app = sys.modules["FreeCAD"]
source.addObject("Part::Feature", "Body")
doc = FakeDocument("QETScene")
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, _ = _reload_modules()
device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertIs(app.ActiveDocument, doc)
self.assertIn("QETScene", app.set_active_document_calls)
def test_non_fcstd_import_clears_stored_template_slots_from_previous_fcstd(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
doc = FakeDocument("QETScene")
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_group.addProperty(
"App::PropertyString",
"QetTemplateSlotsJson",
"QET Exchange",
"",
)
device_group.QetTemplateSlotsJson = (
'{"terminal_slots":[{"name":"D1","label":"D1","base":[1,2,3]}]}'
)
device_import, template_semantics = _reload_modules()
def insert_step_body(name, docName, merge, useLinkGroup):
doc.addObject("Part::Feature", "StepBody")
device_import.ImportGui.insert = insert_step_body
copied_objects = device_import._import_model_into_group(
doc,
device_group,
r"D:\models\breaker.step",
)
self.assertEqual(["StepBody"], [obj.Name for obj in copied_objects])
self.assertEqual("", device_group.QetTemplateSlotsJson)
self.assertEqual([], template_semantics.collect_terminal_hints(device_group))
def test_fcstd_import_detaches_removed_template_lcs_from_parent_group(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
app = sys.modules["FreeCAD"]
body = source.addObject("Part::Feature", "Body")
terminal = source.addObject("Part::LocalCoordinateSystem", "Terminal_D1")
terminal.Placement = app.Placement(app.Vector(1, 2, 3), app.Rotation())
terminal.addProperty("App::PropertyString", "Role", "QET Template", "")
terminal.Role = "Terminal"
terminal.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
terminal.QetTemplateSlotName = "D1"
doc = FakeDocument("QETScene", detach_on_remove=False)
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, _ = _reload_modules()
copied_objects = device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertEqual([body.Name], [obj.Name for obj in copied_objects])
self.assertEqual(["Body"], [obj.Name for obj in device_group.Group])
def test_fcstd_import_does_not_revisit_deleted_origin_features(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
app = sys.modules["FreeCAD"]
body = source.addObject("Part::Feature", "Body")
terminal = source.addObject("Part::LocalCoordinateSystem", "Terminal_D1")
terminal.Placement = app.Placement(app.Vector(1, 2, 3), app.Rotation())
terminal.addProperty("App::PropertyString", "Role", "QET Template", "")
terminal.Role = "Terminal"
terminal.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
terminal.QetTemplateSlotName = "D1"
x_axis = source.addObject("App::Line", "Terminal_D1_XAxis")
terminal.OriginFeatures = [x_axis]
terminal.addObject(x_axis)
doc = FakeDocument("QETScene", raise_group_when_deleted=True)
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, _ = _reload_modules()
copied_objects = device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertEqual([body.Name], [obj.Name for obj in copied_objects])
self.assertEqual(["Body"], [obj.Name for obj in device_group.Group])
def test_fcstd_import_does_not_touch_inlist_after_origin_feature_deleted(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
app = sys.modules["FreeCAD"]
body = source.addObject("Part::Feature", "Body")
terminal = source.addObject("Part::LocalCoordinateSystem", "Terminal_D1")
terminal.Placement = app.Placement(app.Vector(1, 2, 3), app.Rotation())
terminal.addProperty("App::PropertyString", "Role", "QET Template", "")
terminal.Role = "Terminal"
terminal.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
terminal.QetTemplateSlotName = "D1"
x_axis = source.addObject("App::Line", "Terminal_D1_XAxis")
terminal.OriginFeatures = [x_axis]
terminal.addObject(x_axis)
doc = FakeDocument("QETScene", raise_inlist_when_deleted=True)
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, _ = _reload_modules()
copied_objects = device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertEqual([body.Name], [obj.Name for obj in copied_objects])
self.assertEqual(["Body"], [obj.Name for obj in device_group.Group])
def test_fcstd_import_does_not_read_name_from_deleted_template_lcs_when_returning_objects(self):
source = FakeDocument("Source", r"D:\models\breaker.FCStd")
_install_fake_freecad(source)
app = sys.modules["FreeCAD"]
body = source.addObject("Part::Feature", "Body")
terminal = source.addObject("Part::LocalCoordinateSystem", "Terminal_D1")
terminal.Placement = app.Placement(app.Vector(1, 2, 3), app.Rotation())
terminal.addProperty("App::PropertyString", "Role", "QET Template", "")
terminal.Role = "Terminal"
terminal.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
terminal.QetTemplateSlotName = "D1"
doc = FakeDocument("QETScene", raise_name_when_deleted=True)
device_group = doc.addObject("App::Part", "QETDevice_breaker")
device_import, _ = _reload_modules()
copied_objects = device_import._import_fcstd_into_group(
doc,
device_group,
source.FileName,
)
self.assertEqual([body.Name], [obj.Name for obj in copied_objects])
self.assertEqual(["Body"], [obj.Name for obj in device_group.Group])
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save