|
|
import os
|
|
|
from pathlib import Path
|
|
|
import uuid
|
|
|
|
|
|
import FreeCAD as App
|
|
|
|
|
|
try:
|
|
|
import FreeCADGui as Gui
|
|
|
except ImportError:
|
|
|
Gui = None
|
|
|
|
|
|
try:
|
|
|
import ImportGui
|
|
|
except ImportError:
|
|
|
ImportGui = None
|
|
|
import DevicePreview
|
|
|
import TemplateSemantics
|
|
|
import TerminalObjects
|
|
|
|
|
|
|
|
|
ROOT_GROUP_NAME = "QETExchangeDevices"
|
|
|
ROOT_GROUP_LABEL = "QET Exchange Devices"
|
|
|
CABINET_MODEL_GROUP_NAME = "QETCabinetModel"
|
|
|
CABINET_GROUP_PREFIX = "QETCabinet_"
|
|
|
DEVICE_GROUP_PREFIX = "QETDevice_"
|
|
|
TERMINAL_GROUP_PREFIX = "QETTerminals_"
|
|
|
WIRE_GROUP_PREFIX = "QETWires_"
|
|
|
GROUP_KIND_TERMINALS = "Terminals"
|
|
|
GROUP_KIND_WIRES = "Wires"
|
|
|
|
|
|
|
|
|
class DeviceImportError(RuntimeError):
|
|
|
pass
|
|
|
|
|
|
|
|
|
def _debug_log_path():
|
|
|
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
|
|
|
if local_app_data:
|
|
|
return os.path.join(local_app_data, "QETDeps", "freecad_exchange_bootstrap.log")
|
|
|
return os.path.join(str(Path.home()), "AppData", "Local", "QETDeps", "freecad_exchange_bootstrap.log")
|
|
|
|
|
|
|
|
|
def _append_debug_log(message):
|
|
|
try:
|
|
|
log_path = _debug_log_path()
|
|
|
os.makedirs(os.path.dirname(log_path), exist_ok=True)
|
|
|
with open(log_path, "a", encoding="utf-8") as handle:
|
|
|
handle.write(message + "\n")
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
|
|
|
def _safe_token(value):
|
|
|
text = (value or "").strip()
|
|
|
if not text:
|
|
|
return "unknown"
|
|
|
|
|
|
chars = []
|
|
|
for ch in text:
|
|
|
if ch.isalnum():
|
|
|
chars.append(ch)
|
|
|
else:
|
|
|
chars.append("_")
|
|
|
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):
|
|
|
text = (value or "").strip()
|
|
|
if not text:
|
|
|
return ""
|
|
|
return os.path.normpath(os.path.expandvars(os.path.expanduser(text)))
|
|
|
|
|
|
|
|
|
def _normalized_path_key(value):
|
|
|
text = _native_path(value)
|
|
|
if not text:
|
|
|
return ""
|
|
|
return os.path.normcase(os.path.normpath(text))
|
|
|
|
|
|
|
|
|
def _existing_object_names(doc):
|
|
|
return {obj.Name for obj in doc.Objects}
|
|
|
|
|
|
|
|
|
def _new_objects_since(doc, before_names):
|
|
|
return [obj for obj in doc.Objects if obj.Name not in before_names]
|
|
|
|
|
|
|
|
|
def _top_level_imported_objects(imported_objects):
|
|
|
imported_by_name = {obj.Name: obj for obj in imported_objects}
|
|
|
child_names = set()
|
|
|
for obj in imported_objects:
|
|
|
for parent in list(getattr(obj, "InList", []) or []):
|
|
|
if getattr(parent, "Name", None) in imported_by_name:
|
|
|
child_names.add(obj.Name)
|
|
|
for child in list(getattr(obj, "Group", []) or []):
|
|
|
if getattr(child, "Name", None) in imported_by_name:
|
|
|
child_names.add(child.Name)
|
|
|
return [obj for obj in imported_objects if obj.Name not in child_names]
|
|
|
|
|
|
|
|
|
def _top_level_document_objects(doc):
|
|
|
return _top_level_imported_objects(list(getattr(doc, "Objects", []) or []))
|
|
|
|
|
|
|
|
|
def _ensure_string_property(obj, prop_name, group_name, description, value):
|
|
|
if prop_name not in getattr(obj, "PropertiesList", []):
|
|
|
obj.addProperty("App::PropertyString", prop_name, group_name, description)
|
|
|
setattr(obj, prop_name, value or "")
|
|
|
|
|
|
|
|
|
def _ensure_bool_property(obj, prop_name, group_name, description, value):
|
|
|
if prop_name not in getattr(obj, "PropertiesList", []):
|
|
|
obj.addProperty("App::PropertyBool", prop_name, group_name, description)
|
|
|
setattr(obj, prop_name, bool(value))
|
|
|
|
|
|
|
|
|
def _ensure_child_group(doc, parent_group, element_uuid, instance_id, name_prefix, label, group_kind, project_uuid=""):
|
|
|
target_uuid = (element_uuid or "").strip()
|
|
|
preferred_name = name_prefix + _safe_token(target_uuid)
|
|
|
group = doc.getObject(preferred_name)
|
|
|
if group is None:
|
|
|
for candidate in getattr(parent_group, "Group", []) or []:
|
|
|
if getattr(candidate, "QetGroupKind", "").strip() != group_kind:
|
|
|
continue
|
|
|
if target_uuid and getattr(candidate, "QetElementUuid", "").strip() != target_uuid:
|
|
|
continue
|
|
|
group = candidate
|
|
|
break
|
|
|
|
|
|
if group is None:
|
|
|
group = doc.addObject("App::DocumentObjectGroup", preferred_name)
|
|
|
|
|
|
if group not in getattr(parent_group, "Group", []):
|
|
|
parent_group.addObject(group)
|
|
|
|
|
|
group.Label = label
|
|
|
project_uuid = (project_uuid or "").strip() or getattr(group, "QetProjectUuid", "").strip()
|
|
|
element_uuid = (element_uuid or "").strip() or getattr(group, "QetElementUuid", "").strip()
|
|
|
instance_id = (instance_id or "").strip() or getattr(group, "QetInstanceId", "").strip()
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetGroupKind",
|
|
|
"QET Exchange",
|
|
|
"FreeCADExchange group kind",
|
|
|
group_kind,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetProjectUuid",
|
|
|
"QET Exchange",
|
|
|
"Project UUID from QET exchange",
|
|
|
project_uuid,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetElementUuid",
|
|
|
"QET Exchange",
|
|
|
"Parent element UUID from QET exchange",
|
|
|
element_uuid,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetInstanceId",
|
|
|
"QET Exchange",
|
|
|
"Parent instance id from QET exchange",
|
|
|
instance_id,
|
|
|
)
|
|
|
return group
|
|
|
|
|
|
|
|
|
def _ensure_document(scene_path):
|
|
|
preferred_name = _safe_token(Path(scene_path).stem if scene_path else "QETScene")[:48] or "QETScene"
|
|
|
normalized_scene_path = _native_path(scene_path)
|
|
|
if normalized_scene_path and os.path.isfile(normalized_scene_path):
|
|
|
normalized_target = os.path.normcase(os.path.normpath(normalized_scene_path))
|
|
|
for candidate in App.listDocuments().values():
|
|
|
candidate_path = getattr(candidate, "FileName", "") or ""
|
|
|
if candidate_path and os.path.normcase(os.path.normpath(candidate_path)) == normalized_target:
|
|
|
_activate_document(candidate)
|
|
|
return candidate
|
|
|
|
|
|
try:
|
|
|
doc = App.openDocument(normalized_scene_path)
|
|
|
except Exception as exc:
|
|
|
raise DeviceImportError(
|
|
|
"Cannot open existing FreeCAD scene file: {0}".format(normalized_scene_path)
|
|
|
) from exc
|
|
|
|
|
|
if doc is None:
|
|
|
raise DeviceImportError(
|
|
|
"Cannot open existing FreeCAD scene file: {0}".format(normalized_scene_path)
|
|
|
)
|
|
|
|
|
|
_activate_document(doc)
|
|
|
return doc
|
|
|
|
|
|
existing_doc = DevicePreview.find_main_exchange_document(preferred_name)
|
|
|
if existing_doc is not None:
|
|
|
_activate_document(existing_doc)
|
|
|
return existing_doc
|
|
|
|
|
|
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):
|
|
|
if not isinstance(cabinet, dict):
|
|
|
return "QET Cabinet"
|
|
|
|
|
|
label = (cabinet.get("display_text") or "").strip()
|
|
|
if label:
|
|
|
return label
|
|
|
|
|
|
label = (cabinet.get("label") or "").strip()
|
|
|
if label:
|
|
|
return label
|
|
|
|
|
|
label = (cabinet.get("name") or "").strip()
|
|
|
if label:
|
|
|
return label
|
|
|
|
|
|
return "QET Cabinet"
|
|
|
|
|
|
|
|
|
def _ensure_root_group(doc, cabinet=None, project_uuid=""):
|
|
|
root = doc.getObject(ROOT_GROUP_NAME)
|
|
|
if root is None:
|
|
|
root = doc.addObject("App::DocumentObjectGroup", ROOT_GROUP_NAME)
|
|
|
if isinstance(cabinet, dict):
|
|
|
root.Label = _cabinet_label_text(cabinet)
|
|
|
else:
|
|
|
root.Label = ROOT_GROUP_LABEL
|
|
|
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetLabel",
|
|
|
"QET Exchange",
|
|
|
"Cabinet label from QET exchange",
|
|
|
cabinet.get("label", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetName",
|
|
|
"QET Exchange",
|
|
|
"Cabinet name from QET exchange",
|
|
|
cabinet.get("name", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetDisplayText",
|
|
|
"QET Exchange",
|
|
|
"Cabinet display text from QET exchange",
|
|
|
cabinet.get("display_text", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetFileSet",
|
|
|
"QET Exchange",
|
|
|
"Associated fileset from QET exchange",
|
|
|
cabinet.get("associated_fileset", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetRelativePath",
|
|
|
"QET Exchange",
|
|
|
"Relative 3D cabinet path from QET exchange",
|
|
|
cabinet.get("three_d_relative_path", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetResolvedScenePath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local cabinet scene path from QET exchange",
|
|
|
cabinet.get("resolved_scene_path", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetCabinetLocationId",
|
|
|
"QET Exchange",
|
|
|
"Cabinet location id from QET exchange",
|
|
|
str(cabinet.get("location_id") or "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
project_uuid = (project_uuid or "").strip() or getattr(root, "QetProjectUuid", "").strip()
|
|
|
_ensure_string_property(
|
|
|
root,
|
|
|
"QetProjectUuid",
|
|
|
"QET Exchange",
|
|
|
"Project UUID from QET exchange",
|
|
|
project_uuid,
|
|
|
)
|
|
|
return root
|
|
|
|
|
|
|
|
|
def _cabinet_instance_id(cabinet):
|
|
|
if not isinstance(cabinet, dict):
|
|
|
return ""
|
|
|
for field_name in ("cabinet_instance_id", "cabinet_uuid", "location_id"):
|
|
|
value = cabinet.get(field_name)
|
|
|
if value is None:
|
|
|
continue
|
|
|
text = str(value).strip()
|
|
|
if text:
|
|
|
return text
|
|
|
return "default"
|
|
|
|
|
|
|
|
|
def _cabinet_group_label(cabinet):
|
|
|
label = _cabinet_label_text(cabinet)
|
|
|
if label:
|
|
|
return label
|
|
|
return "3D机柜"
|
|
|
|
|
|
|
|
|
def _find_cabinet_group(doc, cabinet_instance_id):
|
|
|
target_id = (cabinet_instance_id or "").strip()
|
|
|
preferred_name = CABINET_GROUP_PREFIX + _safe_token(target_id or "default")
|
|
|
group = doc.getObject(preferred_name)
|
|
|
if group is not None:
|
|
|
return group
|
|
|
|
|
|
legacy_group = doc.getObject(CABINET_MODEL_GROUP_NAME)
|
|
|
if legacy_group is not None:
|
|
|
legacy_instance_id = getattr(legacy_group, "QetCabinetInstanceId", "").strip()
|
|
|
if not target_id or not legacy_instance_id or legacy_instance_id == target_id:
|
|
|
return legacy_group
|
|
|
|
|
|
for candidate in getattr(doc, "Objects", []) or []:
|
|
|
if "QetCabinetInstanceId" not in getattr(candidate, "PropertiesList", []):
|
|
|
continue
|
|
|
if getattr(candidate, "QetCabinetInstanceId", "").strip() == target_id:
|
|
|
return candidate
|
|
|
return None
|
|
|
|
|
|
|
|
|
def _ensure_cabinet_model_group(doc, root_group, cabinet=None, project_uuid=""):
|
|
|
cabinet_instance_id = _cabinet_instance_id(cabinet)
|
|
|
group = _find_cabinet_group(doc, cabinet_instance_id)
|
|
|
if group is None:
|
|
|
group = doc.addObject(
|
|
|
"App::DocumentObjectGroup",
|
|
|
CABINET_GROUP_PREFIX + _safe_token(cabinet_instance_id or "default"),
|
|
|
)
|
|
|
group.Label = _cabinet_group_label(cabinet)
|
|
|
if group not in getattr(root_group, "Group", []):
|
|
|
root_group.addObject(group)
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetCabinetInstanceId",
|
|
|
"QET Exchange",
|
|
|
"Cabinet instance id from QET exchange",
|
|
|
cabinet_instance_id,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetCabinetResolvedScenePath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local cabinet scene path from QET exchange",
|
|
|
cabinet.get("resolved_scene_path", "") if isinstance(cabinet, dict) else "",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
group,
|
|
|
"QetProjectUuid",
|
|
|
"QET Exchange",
|
|
|
"Project UUID from QET exchange",
|
|
|
project_uuid,
|
|
|
)
|
|
|
return group
|
|
|
|
|
|
|
|
|
def _find_device_group(doc, element_uuid):
|
|
|
target_uuid = (element_uuid or "").strip()
|
|
|
if not target_uuid:
|
|
|
return None
|
|
|
|
|
|
preferred_name = DEVICE_GROUP_PREFIX + _safe_token(target_uuid)
|
|
|
obj = doc.getObject(preferred_name)
|
|
|
if obj is not None:
|
|
|
return obj
|
|
|
|
|
|
for candidate in doc.Objects:
|
|
|
if "QetElementUuid" in getattr(candidate, "PropertiesList", []):
|
|
|
if getattr(candidate, "QetElementUuid", "").strip() == target_uuid:
|
|
|
return candidate
|
|
|
return None
|
|
|
|
|
|
def _device_label_text(display_tag, instance_id, element_uuid):
|
|
|
label = (display_tag or "").strip()
|
|
|
if label:
|
|
|
return label
|
|
|
|
|
|
fallback = (instance_id or "").strip() or (element_uuid or "").strip()
|
|
|
if fallback:
|
|
|
return fallback
|
|
|
return "QET Device"
|
|
|
|
|
|
|
|
|
def _device_warning_subject(display_tag, element_uuid):
|
|
|
label = (display_tag or "").strip()
|
|
|
element_uuid = (element_uuid or "").strip()
|
|
|
if label and element_uuid:
|
|
|
return "设备 {0} ({1})".format(label, element_uuid)
|
|
|
if label:
|
|
|
return "设备 {0}".format(label)
|
|
|
if element_uuid:
|
|
|
return "设备 {0}".format(element_uuid)
|
|
|
return "设备"
|
|
|
|
|
|
|
|
|
def _ensure_device_group(doc, root_group, element_uuid, instance_id, model_path, display_tag, layout_index):
|
|
|
created_now = False
|
|
|
device_group = _find_device_group(doc, element_uuid)
|
|
|
if device_group is not None and getattr(device_group, "TypeId", "") != "App::Part":
|
|
|
_remove_object_tree(doc, device_group)
|
|
|
device_group = None
|
|
|
|
|
|
if device_group is None:
|
|
|
device_group = doc.addObject(
|
|
|
"App::Part",
|
|
|
DEVICE_GROUP_PREFIX + _safe_token(element_uuid),
|
|
|
)
|
|
|
created_now = True
|
|
|
|
|
|
if device_group not in getattr(root_group, "Group", []):
|
|
|
root_group.addObject(device_group)
|
|
|
|
|
|
device_group.Label = _device_label_text(display_tag, instance_id, element_uuid)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetElementUuid",
|
|
|
"QET Exchange",
|
|
|
"2D element UUID from QET",
|
|
|
element_uuid,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetInstanceId",
|
|
|
"QET Exchange",
|
|
|
"3D instance id from QET/FreeCAD exchange",
|
|
|
instance_id,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetResolvedModelPath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local model path from QET exchange",
|
|
|
model_path,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetDisplayTag",
|
|
|
"QET Exchange",
|
|
|
"2D display tag from QET exchange",
|
|
|
display_tag,
|
|
|
)
|
|
|
_ensure_bool_property(
|
|
|
device_group,
|
|
|
"QetAutoPlaced",
|
|
|
"QET Exchange",
|
|
|
"Whether the device has been placed by the QET auto layout.",
|
|
|
created_now,
|
|
|
)
|
|
|
if created_now:
|
|
|
device_group.Placement = App.Placement()
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetProjectUuid",
|
|
|
"QET Exchange",
|
|
|
"Project UUID from QET exchange",
|
|
|
getattr(root_group, "QetProjectUuid", "").strip(),
|
|
|
)
|
|
|
_ensure_child_group(
|
|
|
doc,
|
|
|
device_group,
|
|
|
element_uuid,
|
|
|
instance_id,
|
|
|
TERMINAL_GROUP_PREFIX,
|
|
|
"QET Terminals",
|
|
|
GROUP_KIND_TERMINALS,
|
|
|
project_uuid=getattr(root_group, "QetProjectUuid", "").strip(),
|
|
|
)
|
|
|
_ensure_child_group(
|
|
|
doc,
|
|
|
device_group,
|
|
|
element_uuid,
|
|
|
instance_id,
|
|
|
WIRE_GROUP_PREFIX,
|
|
|
"QET Wires",
|
|
|
GROUP_KIND_WIRES,
|
|
|
project_uuid=getattr(root_group, "QetProjectUuid", "").strip(),
|
|
|
)
|
|
|
return device_group, created_now
|
|
|
|
|
|
|
|
|
def _remove_object_tree(doc, obj):
|
|
|
if obj is None:
|
|
|
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 [])
|
|
|
for child in children:
|
|
|
_remove_object_tree(doc, child)
|
|
|
|
|
|
_detach_from_parent_groups(obj)
|
|
|
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 _existing_group_objects(doc, group):
|
|
|
result = []
|
|
|
for child in list(getattr(group, "Group", []) or []):
|
|
|
if _object_exists(doc, child):
|
|
|
result.append(child)
|
|
|
return result
|
|
|
|
|
|
|
|
|
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 _existing_model_objects(doc, group):
|
|
|
return [
|
|
|
obj
|
|
|
for obj in _existing_group_objects(doc, group)
|
|
|
if not _is_exchange_sidecar_group(obj)
|
|
|
]
|
|
|
|
|
|
|
|
|
def _remove_model_objects(doc, objects):
|
|
|
for obj in list(objects or []):
|
|
|
_remove_object_tree(doc, obj)
|
|
|
|
|
|
|
|
|
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):
|
|
|
seed = "QET:{0}:{1}".format((project_uuid or "").strip(), (element_uuid or "").strip())
|
|
|
return str(uuid.uuid5(uuid.NAMESPACE_URL, seed))
|
|
|
|
|
|
|
|
|
def _supported_for_import(model_path):
|
|
|
suffix = Path(model_path).suffix.lower()
|
|
|
return suffix in {
|
|
|
".step",
|
|
|
".stp",
|
|
|
".iges",
|
|
|
".igs",
|
|
|
".brep",
|
|
|
".brp",
|
|
|
".fcstd",
|
|
|
}
|
|
|
|
|
|
|
|
|
def _import_model_into_group(doc, device_group, model_path, merge=False, use_link_group=True):
|
|
|
if Path(model_path).suffix.lower() == ".fcstd":
|
|
|
return _import_fcstd_into_group(doc, device_group, model_path)
|
|
|
|
|
|
before_names = _existing_object_names(doc)
|
|
|
try:
|
|
|
try:
|
|
|
ImportGui.insert(
|
|
|
name=model_path,
|
|
|
docName=doc.Name,
|
|
|
merge=bool(merge),
|
|
|
useLinkGroup=bool(use_link_group),
|
|
|
)
|
|
|
except Exception:
|
|
|
for obj in _new_objects_since(doc, before_names):
|
|
|
_remove_object_tree(doc, obj)
|
|
|
raise
|
|
|
|
|
|
imported_objects = _new_objects_since(doc, before_names)
|
|
|
top_level_objects = _top_level_imported_objects(imported_objects)
|
|
|
for obj in top_level_objects:
|
|
|
if obj not in getattr(device_group, "Group", []):
|
|
|
device_group.addObject(obj)
|
|
|
TemplateSemantics.clear_stored_template_slot_hints(device_group)
|
|
|
TerminalObjects.hide_template_terminal_hints(device_group)
|
|
|
return top_level_objects
|
|
|
finally:
|
|
|
_activate_document(doc)
|
|
|
|
|
|
|
|
|
def _open_fcstd_source_document(model_path):
|
|
|
normalized_target = os.path.normcase(os.path.normpath(model_path))
|
|
|
for candidate in App.listDocuments().values():
|
|
|
candidate_path = getattr(candidate, "FileName", "") or ""
|
|
|
if candidate_path and os.path.normcase(os.path.normpath(candidate_path)) == normalized_target:
|
|
|
return candidate, False
|
|
|
source_doc = App.openDocument(model_path, hidden=True, temporary=True)
|
|
|
return source_doc, True
|
|
|
|
|
|
|
|
|
def _import_fcstd_into_group(doc, device_group, model_path):
|
|
|
source_doc = None
|
|
|
should_close = False
|
|
|
try:
|
|
|
source_doc, should_close = _open_fcstd_source_document(model_path)
|
|
|
if source_doc is None:
|
|
|
raise DeviceImportError("Cannot open FCStd file")
|
|
|
|
|
|
TemplateSemantics.clear_stored_template_slot_hints(device_group)
|
|
|
top_level_objects = _top_level_document_objects(source_doc)
|
|
|
copied_objects = []
|
|
|
for source_obj in top_level_objects:
|
|
|
copied_obj = doc.copyObject(source_obj, True)
|
|
|
if copied_obj not in getattr(device_group, "Group", []):
|
|
|
device_group.addObject(copied_obj)
|
|
|
copied_objects.append(copied_obj)
|
|
|
|
|
|
template_slots = TemplateSemantics.collect_live_terminal_hints(device_group)
|
|
|
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:
|
|
|
if should_close and source_doc is not None:
|
|
|
try:
|
|
|
App.closeDocument(source_doc.Name)
|
|
|
except Exception:
|
|
|
pass
|
|
|
_activate_document(doc)
|
|
|
|
|
|
|
|
|
def _model_index(payload):
|
|
|
index = {}
|
|
|
for item in payload.get("device_models", []):
|
|
|
element_uuid = item.get("element_uuid", "").strip()
|
|
|
if element_uuid and element_uuid not in index:
|
|
|
index[element_uuid] = item
|
|
|
return index
|
|
|
|
|
|
|
|
|
def _import_cabinet_model(doc, root_group, cabinet, report):
|
|
|
if not isinstance(cabinet, dict):
|
|
|
return
|
|
|
|
|
|
resolved_scene_path = _native_path(cabinet.get("resolved_scene_path", ""))
|
|
|
_append_debug_log(
|
|
|
"DeviceImport cabinet resolved_scene_path={0}".format(resolved_scene_path)
|
|
|
)
|
|
|
if not resolved_scene_path:
|
|
|
report["cabinet_skipped_missing_model"] += 1
|
|
|
return
|
|
|
|
|
|
if not os.path.isfile(resolved_scene_path):
|
|
|
report["cabinet_skipped_missing_file"] += 1
|
|
|
report["warnings"].append(
|
|
|
"机柜 3D 文件不存在:{0}".format(resolved_scene_path)
|
|
|
)
|
|
|
return
|
|
|
|
|
|
if not _supported_for_import(resolved_scene_path):
|
|
|
report["cabinet_skipped_unsupported_format"] += 1
|
|
|
report["warnings"].append(
|
|
|
"机柜 3D 文件格式暂不支持:{0}".format(resolved_scene_path)
|
|
|
)
|
|
|
return
|
|
|
|
|
|
project_uuid = getattr(root_group, "QetProjectUuid", "").strip()
|
|
|
existing_group = _find_cabinet_group(doc, _cabinet_instance_id(cabinet))
|
|
|
previous_path = ""
|
|
|
if existing_group is not None:
|
|
|
previous_path = getattr(existing_group, "QetCabinetResolvedScenePath", "").strip()
|
|
|
cabinet_group = _ensure_cabinet_model_group(doc, root_group, cabinet, project_uuid)
|
|
|
existing_model_objects = _existing_model_objects(doc, cabinet_group)
|
|
|
same_source = _normalized_path_key(previous_path) == _normalized_path_key(resolved_scene_path)
|
|
|
if existing_model_objects and same_source:
|
|
|
report.setdefault("cabinet_reused", 0)
|
|
|
report["cabinet_reused"] += 1
|
|
|
_append_debug_log(
|
|
|
"DeviceImport cabinet import skipped: reused existing cabinet group for instance_id={0}".format(
|
|
|
getattr(cabinet_group, "QetCabinetInstanceId", "").strip()
|
|
|
)
|
|
|
)
|
|
|
return
|
|
|
|
|
|
had_existing_model = bool(existing_model_objects)
|
|
|
_ensure_string_property(
|
|
|
cabinet_group,
|
|
|
"QetCabinetResolvedScenePath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local cabinet scene path from QET exchange",
|
|
|
resolved_scene_path,
|
|
|
)
|
|
|
try:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport importing cabinet model: {0}".format(
|
|
|
resolved_scene_path
|
|
|
)
|
|
|
)
|
|
|
_import_model_into_group(
|
|
|
doc,
|
|
|
cabinet_group,
|
|
|
resolved_scene_path,
|
|
|
merge=False,
|
|
|
use_link_group=True,
|
|
|
)
|
|
|
_remove_model_objects(doc, existing_model_objects)
|
|
|
report["cabinet_imported"] += 1
|
|
|
if had_existing_model:
|
|
|
report.setdefault("cabinet_reimported", 0)
|
|
|
report["cabinet_reimported"] += 1
|
|
|
else:
|
|
|
report.setdefault("cabinet_added", 0)
|
|
|
report["cabinet_added"] += 1
|
|
|
_append_debug_log("DeviceImport cabinet import succeeded")
|
|
|
except Exception as exc:
|
|
|
if had_existing_model:
|
|
|
_ensure_string_property(
|
|
|
cabinet_group,
|
|
|
"QetCabinetResolvedScenePath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local cabinet scene path from QET exchange",
|
|
|
previous_path,
|
|
|
)
|
|
|
report["cabinet_skipped_import_error"] += 1
|
|
|
report["warnings"].append(
|
|
|
"机柜 3D 导入失败:{0}".format(exc)
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport cabinet import failed: {0}".format(exc)
|
|
|
)
|
|
|
|
|
|
|
|
|
def import_devices_from_payload(payload, scene_path=""):
|
|
|
_append_debug_log("DeviceImport.import_devices_from_payload entered")
|
|
|
doc = _ensure_document(scene_path)
|
|
|
cabinet = payload.get("cabinet")
|
|
|
project_uuid = (payload.get("project_uuid") or "").strip()
|
|
|
root_group = _ensure_root_group(doc, cabinet, project_uuid)
|
|
|
models_by_element = _model_index(payload)
|
|
|
|
|
|
report = {
|
|
|
"document_name": doc.Name,
|
|
|
"scene_path": scene_path or "",
|
|
|
"total_devices": 0,
|
|
|
"imported_devices": 0,
|
|
|
"updated_devices": 0,
|
|
|
"imported_without_instance_id": 0,
|
|
|
"skipped_missing_model": 0,
|
|
|
"skipped_missing_file": 0,
|
|
|
"skipped_unsupported_format": 0,
|
|
|
"skipped_import_error": 0,
|
|
|
"cabinet_imported": 0,
|
|
|
"cabinet_added": 0,
|
|
|
"cabinet_reimported": 0,
|
|
|
"cabinet_reused": 0,
|
|
|
"cabinet_skipped_missing_model": 0,
|
|
|
"cabinet_skipped_missing_file": 0,
|
|
|
"cabinet_skipped_unsupported_format": 0,
|
|
|
"cabinet_skipped_import_error": 0,
|
|
|
"warnings": [],
|
|
|
}
|
|
|
|
|
|
_import_cabinet_model(doc, root_group, cabinet, report)
|
|
|
|
|
|
for index, device in enumerate(payload.get("devices", [])):
|
|
|
report["total_devices"] += 1
|
|
|
|
|
|
element_uuid = device.get("element_uuid", "").strip()
|
|
|
instance_id = (device.get("instance_id") or "").strip()
|
|
|
display_tag = (device.get("display_tag") or "").strip()
|
|
|
model_info = models_by_element.get(element_uuid, {})
|
|
|
resolved_model_path = _native_path(model_info.get("resolved_model_path", ""))
|
|
|
_append_debug_log(
|
|
|
"DeviceImport device element_uuid={0}, instance_id={1}, display_tag={2}, resolved_model_path={3}".format(
|
|
|
element_uuid, instance_id, display_tag, resolved_model_path
|
|
|
)
|
|
|
)
|
|
|
|
|
|
if not resolved_model_path:
|
|
|
report["skipped_missing_model"] += 1
|
|
|
report["warnings"].append(
|
|
|
"{0} 缺少 resolved_model_path,已跳过。".format(
|
|
|
_device_warning_subject(display_tag, element_uuid)
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if not os.path.isfile(resolved_model_path):
|
|
|
report["skipped_missing_file"] += 1
|
|
|
report["warnings"].append(
|
|
|
"{0} 的模型文件不存在:{1}".format(
|
|
|
_device_warning_subject(display_tag, element_uuid),
|
|
|
resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if not _supported_for_import(resolved_model_path):
|
|
|
report["skipped_unsupported_format"] += 1
|
|
|
report["warnings"].append(
|
|
|
"{0} 的模型格式暂不支持:{1}".format(
|
|
|
_device_warning_subject(display_tag, element_uuid),
|
|
|
resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
existing_group = _find_device_group(doc, element_uuid)
|
|
|
if not instance_id:
|
|
|
existing_instance_id = ""
|
|
|
if existing_group is not None:
|
|
|
existing_instance_id = getattr(existing_group, "QetInstanceId", "").strip()
|
|
|
instance_id = existing_instance_id or _generate_instance_id(project_uuid, element_uuid)
|
|
|
report.setdefault("generated_instance_ids", 0)
|
|
|
report["generated_instance_ids"] += 1
|
|
|
previous_model_path = ""
|
|
|
if existing_group is not None:
|
|
|
previous_model_path = getattr(existing_group, "QetResolvedModelPath", "").strip()
|
|
|
device_group, created_now = _ensure_device_group(
|
|
|
doc,
|
|
|
root_group,
|
|
|
element_uuid,
|
|
|
instance_id,
|
|
|
resolved_model_path,
|
|
|
display_tag,
|
|
|
index,
|
|
|
)
|
|
|
existing_model_objects = _existing_model_objects(doc, device_group)
|
|
|
|
|
|
try:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport importing model for element_uuid={0}: {1}".format(
|
|
|
element_uuid, resolved_model_path
|
|
|
)
|
|
|
)
|
|
|
_import_model_into_group(doc, device_group, resolved_model_path)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport import succeeded for element_uuid={0}".format(element_uuid)
|
|
|
)
|
|
|
_remove_model_objects(doc, existing_model_objects)
|
|
|
except Exception as exc:
|
|
|
if existing_model_objects:
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetResolvedModelPath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local model path from QET exchange",
|
|
|
previous_model_path,
|
|
|
)
|
|
|
report["skipped_import_error"] += 1
|
|
|
report["warnings"].append(
|
|
|
"{0} 导入失败:{1}".format(
|
|
|
_device_warning_subject(display_tag, element_uuid),
|
|
|
exc,
|
|
|
)
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport import failed for element_uuid={0}: {1}".format(
|
|
|
element_uuid, exc
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if created_now or existing_group is None:
|
|
|
report["imported_devices"] += 1
|
|
|
else:
|
|
|
report["updated_devices"] += 1
|
|
|
|
|
|
if not instance_id:
|
|
|
report["imported_without_instance_id"] += 1
|
|
|
|
|
|
doc.recompute()
|
|
|
try:
|
|
|
Gui.SendMsgToActiveView("ViewFit")
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
_append_debug_log(
|
|
|
"DeviceImport finished: cabinet_imported={0}, imported={1}, updated={2}, skipped_missing_model={3}, skipped_missing_file={4}, skipped_import_error={5}".format(
|
|
|
report["cabinet_imported"],
|
|
|
report["imported_devices"],
|
|
|
report["updated_devices"],
|
|
|
report["skipped_missing_model"],
|
|
|
report["skipped_missing_file"],
|
|
|
report["skipped_import_error"],
|
|
|
)
|
|
|
)
|
|
|
return report
|