|
|
import json
|
|
|
import os
|
|
|
from pathlib import Path
|
|
|
import uuid
|
|
|
import json
|
|
|
from datetime import datetime
|
|
|
|
|
|
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"
|
|
|
ASSEMBLY_STATE_PENDING = "Pending"
|
|
|
ASSEMBLY_STATE_PLACED = "Placed"
|
|
|
|
|
|
|
|
|
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(
|
|
|
"[{0}] {1}\n".format(
|
|
|
datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
|
message,
|
|
|
)
|
|
|
)
|
|
|
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)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _ensure_document: preferred_name={0}, normalized_scene_path={1}".format(
|
|
|
preferred_name,
|
|
|
normalized_scene_path or "<empty>",
|
|
|
)
|
|
|
)
|
|
|
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:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _ensure_document reusing already open scene doc: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(candidate, "Name", ""),
|
|
|
candidate_path,
|
|
|
len(list(getattr(candidate, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
_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)
|
|
|
)
|
|
|
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _ensure_document opened existing scene doc: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(doc, "Name", ""),
|
|
|
getattr(doc, "FileName", "") or normalized_scene_path,
|
|
|
len(list(getattr(doc, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
_activate_document(doc)
|
|
|
return doc
|
|
|
|
|
|
existing_doc = DevicePreview.find_main_exchange_document(preferred_name)
|
|
|
if existing_doc is not None:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _ensure_document reusing unsaved exchange doc: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(existing_doc, "Name", ""),
|
|
|
getattr(existing_doc, "FileName", "") or "<unsaved>",
|
|
|
len(list(getattr(existing_doc, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
_activate_document(existing_doc)
|
|
|
return existing_doc
|
|
|
|
|
|
doc = App.newDocument(preferred_name)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _ensure_document created new scene doc: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(doc, "Name", ""),
|
|
|
getattr(doc, "FileName", "") or "<unsaved>",
|
|
|
len(list(getattr(doc, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
_activate_document(doc)
|
|
|
return doc
|
|
|
|
|
|
|
|
|
def _activate_document(doc):
|
|
|
if doc is None:
|
|
|
return
|
|
|
|
|
|
current_doc = getattr(App, "ActiveDocument", None)
|
|
|
if current_doc is doc:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _activate_document skipped: already active name={0}, path={1}".format(
|
|
|
getattr(doc, "Name", ""),
|
|
|
getattr(doc, "FileName", "") or "<unsaved>",
|
|
|
)
|
|
|
)
|
|
|
return
|
|
|
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _activate_document: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(doc, "Name", ""),
|
|
|
getattr(doc, "FileName", "") or "<unsaved>",
|
|
|
len(list(getattr(doc, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
|
|
|
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 and getattr(obj, "Name", "").startswith(DEVICE_GROUP_PREFIX):
|
|
|
return obj
|
|
|
|
|
|
for candidate in doc.Objects:
|
|
|
if not getattr(candidate, "Name", "").startswith(DEVICE_GROUP_PREFIX):
|
|
|
continue
|
|
|
try:
|
|
|
if not candidate.isDerivedFrom("App::DocumentObjectGroup"):
|
|
|
continue
|
|
|
except Exception:
|
|
|
continue
|
|
|
if "QetElementUuid" in getattr(candidate, "PropertiesList", []):
|
|
|
if getattr(candidate, "QetElementUuid", "").strip() == target_uuid:
|
|
|
return candidate
|
|
|
return None
|
|
|
|
|
|
|
|
|
def _find_device_group_by_instance_id(doc, instance_id):
|
|
|
target_instance_id = (instance_id or "").strip()
|
|
|
if not target_instance_id:
|
|
|
return None
|
|
|
|
|
|
for candidate in doc.Objects:
|
|
|
if not getattr(candidate, "Name", "").startswith(DEVICE_GROUP_PREFIX):
|
|
|
continue
|
|
|
if "QetInstanceId" not in getattr(candidate, "PropertiesList", []):
|
|
|
continue
|
|
|
if getattr(candidate, "QetInstanceId", "").strip() == target_instance_id:
|
|
|
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 _device_report_label(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()
|
|
|
return fallback or "未命名设备"
|
|
|
|
|
|
|
|
|
def _payload_device_instance_id(device):
|
|
|
if not isinstance(device, dict):
|
|
|
return ""
|
|
|
return (device.get("device_instance_id") or "").strip()
|
|
|
|
|
|
|
|
|
def _payload_device_element_uuid(device):
|
|
|
if not isinstance(device, dict):
|
|
|
return ""
|
|
|
for terminal in device.get("terminals", []) or []:
|
|
|
if not isinstance(terminal, dict):
|
|
|
continue
|
|
|
element_uuid = (terminal.get("element_uuid") or "").strip()
|
|
|
if element_uuid:
|
|
|
return element_uuid
|
|
|
return ""
|
|
|
|
|
|
|
|
|
def _payload_terminal_uuid_set(device):
|
|
|
result = set()
|
|
|
if not isinstance(device, dict):
|
|
|
return result
|
|
|
for terminal in device.get("terminals", []) or []:
|
|
|
if not isinstance(terminal, dict):
|
|
|
continue
|
|
|
terminal_uuid = (terminal.get("terminal_uuid") or "").strip()
|
|
|
if terminal_uuid:
|
|
|
result.add(terminal_uuid)
|
|
|
return result
|
|
|
|
|
|
|
|
|
def _terminal_signature_token(element_uuid, terminal_uuid):
|
|
|
return "{0}|{1}".format((element_uuid or "").strip(), (terminal_uuid or "").strip())
|
|
|
|
|
|
|
|
|
def _payload_terminal_signature_counts(device):
|
|
|
result = {}
|
|
|
if not isinstance(device, dict):
|
|
|
return result
|
|
|
for terminal in device.get("terminals", []) or []:
|
|
|
if not isinstance(terminal, dict):
|
|
|
continue
|
|
|
terminal_uuid = (terminal.get("terminal_uuid") or "").strip()
|
|
|
element_uuid = (terminal.get("element_uuid") or "").strip()
|
|
|
if not terminal_uuid:
|
|
|
continue
|
|
|
token = _terminal_signature_token(element_uuid, terminal_uuid)
|
|
|
result[token] = result.get(token, 0) + 1
|
|
|
return result
|
|
|
|
|
|
|
|
|
def _existing_qet_terminal_uuids(device_group):
|
|
|
terminal_group = TerminalObjects.find_child_group_by_kind(
|
|
|
device_group,
|
|
|
TerminalObjects.TERMINAL_GROUP_KIND,
|
|
|
)
|
|
|
result = set()
|
|
|
for terminal_obj in TerminalObjects.collect_terminal_objects(terminal_group):
|
|
|
terminal_uuid = (getattr(terminal_obj, "QetTerminalUuid", "") or "").strip()
|
|
|
if not terminal_uuid or TerminalObjects.is_local_terminal_uuid(terminal_uuid):
|
|
|
continue
|
|
|
result.add(terminal_uuid)
|
|
|
return result
|
|
|
|
|
|
|
|
|
def _existing_qet_terminal_signature_counts(device_group):
|
|
|
raw_json = (getattr(device_group, "QetPayloadTerminalSignaturesJson", "") or "").strip()
|
|
|
if raw_json:
|
|
|
try:
|
|
|
parsed = json.loads(raw_json)
|
|
|
if isinstance(parsed, dict):
|
|
|
result = {}
|
|
|
for key, value in parsed.items():
|
|
|
key_text = str(key or "").strip()
|
|
|
if not key_text:
|
|
|
continue
|
|
|
try:
|
|
|
count_value = int(value)
|
|
|
except Exception:
|
|
|
count_value = 0
|
|
|
if count_value > 0:
|
|
|
result[key_text] = count_value
|
|
|
if result:
|
|
|
return result
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
terminal_group = TerminalObjects.find_child_group_by_kind(
|
|
|
device_group,
|
|
|
TerminalObjects.TERMINAL_GROUP_KIND,
|
|
|
)
|
|
|
result = {}
|
|
|
for terminal_obj in TerminalObjects.collect_terminal_objects(terminal_group):
|
|
|
terminal_uuid = (getattr(terminal_obj, "QetTerminalUuid", "") or "").strip()
|
|
|
element_uuid = (getattr(terminal_obj, "QetElementUuid", "") or "").strip()
|
|
|
if not terminal_uuid or TerminalObjects.is_local_terminal_uuid(terminal_uuid):
|
|
|
continue
|
|
|
token = _terminal_signature_token(element_uuid, terminal_uuid)
|
|
|
result[token] = result.get(token, 0) + 1
|
|
|
return result
|
|
|
|
|
|
|
|
|
def _store_device_payload_terminal_signatures(device_group, signature_counts):
|
|
|
normalized = {}
|
|
|
for key, value in (signature_counts or {}).items():
|
|
|
key_text = str(key or "").strip()
|
|
|
if not key_text:
|
|
|
continue
|
|
|
try:
|
|
|
count_value = int(value)
|
|
|
except Exception:
|
|
|
count_value = 0
|
|
|
if count_value > 0:
|
|
|
normalized[key_text] = count_value
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetPayloadTerminalSignaturesJson",
|
|
|
"QET Exchange",
|
|
|
"Serialized terminal-entry signatures from the last QET payload",
|
|
|
json.dumps(normalized, ensure_ascii=False, sort_keys=True),
|
|
|
)
|
|
|
|
|
|
|
|
|
def _device_change_detail(
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid="",
|
|
|
change_types=None,
|
|
|
added_terminal_uuids=None,
|
|
|
removed_terminal_uuids=None,
|
|
|
previous_terminal_entry_count=0,
|
|
|
current_terminal_entry_count=0,
|
|
|
previous_display_tag="",
|
|
|
previous_model_path="",
|
|
|
resolved_model_path="",
|
|
|
):
|
|
|
return {
|
|
|
"display_tag": (display_tag or "").strip(),
|
|
|
"instance_id": (instance_id or "").strip(),
|
|
|
"element_uuid": (element_uuid or "").strip(),
|
|
|
"label": _device_report_label(display_tag, instance_id, element_uuid),
|
|
|
"change_types": list(change_types or []),
|
|
|
"added_terminal_uuids": list(added_terminal_uuids or []),
|
|
|
"removed_terminal_uuids": list(removed_terminal_uuids or []),
|
|
|
"previous_terminal_entry_count": int(previous_terminal_entry_count or 0),
|
|
|
"current_terminal_entry_count": int(current_terminal_entry_count or 0),
|
|
|
"previous_display_tag": (previous_display_tag or "").strip(),
|
|
|
"previous_model_path": (previous_model_path or "").strip(),
|
|
|
"resolved_model_path": (resolved_model_path or "").strip(),
|
|
|
}
|
|
|
|
|
|
|
|
|
def _update_device_group_metadata(device_group, root_group, element_uuid, instance_id, model_path, display_tag):
|
|
|
if device_group is None:
|
|
|
return
|
|
|
|
|
|
current_element_uuid = getattr(device_group, "QetElementUuid", "").strip()
|
|
|
current_instance_id = getattr(device_group, "QetInstanceId", "").strip()
|
|
|
current_model_path = getattr(device_group, "QetResolvedModelPath", "").strip()
|
|
|
|
|
|
final_element_uuid = (element_uuid or "").strip() or current_element_uuid
|
|
|
final_instance_id = (instance_id or "").strip() or current_instance_id
|
|
|
final_model_path = (model_path or "").strip() or current_model_path
|
|
|
final_display_tag = (display_tag or "").strip()
|
|
|
|
|
|
device_group.Label = _device_label_text(
|
|
|
final_display_tag,
|
|
|
final_instance_id,
|
|
|
final_element_uuid,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetElementUuid",
|
|
|
"QET Exchange",
|
|
|
"2D element UUID from QET",
|
|
|
final_element_uuid,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetInstanceId",
|
|
|
"QET Exchange",
|
|
|
"3D instance id from QET/FreeCAD exchange",
|
|
|
final_instance_id,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetResolvedModelPath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local model path from QET exchange",
|
|
|
final_model_path,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetDisplayTag",
|
|
|
"QET Exchange",
|
|
|
"2D display tag from QET exchange",
|
|
|
final_display_tag,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetProjectUuid",
|
|
|
"QET Exchange",
|
|
|
"Project UUID from QET exchange",
|
|
|
getattr(root_group, "QetProjectUuid", "").strip(),
|
|
|
)
|
|
|
|
|
|
|
|
|
def _set_device_assembly_state(device_group, state):
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetAssemblyState",
|
|
|
"QET Assembly",
|
|
|
"Assembly state in the FreeCAD scene.",
|
|
|
state,
|
|
|
)
|
|
|
|
|
|
|
|
|
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_by_instance_id(doc, instance_id)
|
|
|
if device_group is None:
|
|
|
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:
|
|
|
group_token = (
|
|
|
(element_uuid or "").strip()
|
|
|
or (instance_id or "").strip()
|
|
|
or (display_tag or "").strip()
|
|
|
or "device-{0}".format(layout_index)
|
|
|
)
|
|
|
device_group = doc.addObject(
|
|
|
"App::Part",
|
|
|
DEVICE_GROUP_PREFIX + _safe_token(group_token),
|
|
|
)
|
|
|
created_now = True
|
|
|
|
|
|
if device_group not in getattr(root_group, "Group", []):
|
|
|
root_group.addObject(device_group)
|
|
|
|
|
|
_update_device_group_metadata(
|
|
|
device_group,
|
|
|
root_group,
|
|
|
element_uuid,
|
|
|
instance_id,
|
|
|
model_path,
|
|
|
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 _register_pending_device(report, device_group, display_tag, instance_id, element_uuid, resolved_model_path):
|
|
|
_set_device_assembly_state(device_group, ASSEMBLY_STATE_PENDING)
|
|
|
report.setdefault("pending_devices", 0)
|
|
|
report.setdefault("pending_device_details", [])
|
|
|
report["pending_devices"] += 1
|
|
|
report["pending_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid=element_uuid,
|
|
|
change_types=["待装配"],
|
|
|
resolved_model_path=resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
|
|
|
|
|
|
def _looks_like_qet_device_group(obj):
|
|
|
if obj is None:
|
|
|
return False
|
|
|
if not getattr(obj, "Name", "").startswith(DEVICE_GROUP_PREFIX):
|
|
|
return False
|
|
|
return bool((getattr(obj, "QetInstanceId", "") or "").strip())
|
|
|
|
|
|
|
|
|
def _find_device_group_from_object(obj):
|
|
|
if _looks_like_qet_device_group(obj):
|
|
|
return obj
|
|
|
pending = list(getattr(obj, "InList", []) or [])
|
|
|
seen = set()
|
|
|
while pending:
|
|
|
parent = pending.pop(0)
|
|
|
parent_name = getattr(parent, "Name", "")
|
|
|
if parent_name in seen:
|
|
|
continue
|
|
|
seen.add(parent_name)
|
|
|
if _looks_like_qet_device_group(parent):
|
|
|
return parent
|
|
|
pending.extend(list(getattr(parent, "InList", []) or []))
|
|
|
return None
|
|
|
|
|
|
|
|
|
def list_pending_devices(doc):
|
|
|
if doc is None:
|
|
|
return []
|
|
|
root = doc.getObject(ROOT_GROUP_NAME)
|
|
|
if root is None:
|
|
|
return []
|
|
|
|
|
|
pending_devices = []
|
|
|
for child in list(getattr(root, "Group", []) or []):
|
|
|
if not _looks_like_qet_device_group(child):
|
|
|
continue
|
|
|
if (getattr(child, "QetAssemblyState", "") or "").strip() != ASSEMBLY_STATE_PENDING:
|
|
|
continue
|
|
|
pending_devices.append(
|
|
|
{
|
|
|
"device": child,
|
|
|
"instance_id": (getattr(child, "QetInstanceId", "") or "").strip(),
|
|
|
"element_uuid": (getattr(child, "QetElementUuid", "") or "").strip(),
|
|
|
"display_tag": (getattr(child, "QetDisplayTag", "") or "").strip(),
|
|
|
"label": getattr(child, "Label", "") or getattr(child, "Name", ""),
|
|
|
"resolved_model_path": (
|
|
|
getattr(child, "QetResolvedModelPath", "") or ""
|
|
|
).strip(),
|
|
|
}
|
|
|
)
|
|
|
return pending_devices
|
|
|
|
|
|
|
|
|
def _target_mount_kind(target_obj):
|
|
|
if target_obj is None:
|
|
|
return ""
|
|
|
kind = (getattr(target_obj, "QetCarrierKind", "") or "").strip()
|
|
|
if kind:
|
|
|
return kind
|
|
|
text = " ".join(
|
|
|
[
|
|
|
getattr(target_obj, "Label", "") or "",
|
|
|
getattr(target_obj, "Name", "") or "",
|
|
|
getattr(target_obj, "TypeId", "") or "",
|
|
|
]
|
|
|
).lower()
|
|
|
if "rail" in text or "din" in text or "导轨" in text:
|
|
|
return "rail"
|
|
|
if "wireduct" in text or "wire_duct" in text or "线槽" in text:
|
|
|
return "wire_duct"
|
|
|
if "plate" in text or "panel" in text or "安装板" in text or "面板" in text:
|
|
|
return "mounting_plate"
|
|
|
if "cabinet" in text or "柜" in text:
|
|
|
return "cabinet"
|
|
|
return ""
|
|
|
|
|
|
|
|
|
def _placement_for_mount_target(mount_target, fallback_rotation=None):
|
|
|
placement = getattr(mount_target, "Placement", None)
|
|
|
base = getattr(placement, "Base", None)
|
|
|
if placement is None or base is None:
|
|
|
return None
|
|
|
rotation = getattr(placement, "Rotation", None) or fallback_rotation or App.Rotation()
|
|
|
return App.Placement(base, rotation)
|
|
|
|
|
|
|
|
|
def _vector_payload(vector):
|
|
|
return {
|
|
|
"x": float(getattr(vector, "x", 0.0) or 0.0),
|
|
|
"y": float(getattr(vector, "y", 0.0) or 0.0),
|
|
|
"z": float(getattr(vector, "z", 0.0) or 0.0),
|
|
|
}
|
|
|
|
|
|
|
|
|
def _normalized_vector(vector):
|
|
|
if vector is None:
|
|
|
return None
|
|
|
x = float(getattr(vector, "x", 0.0) or 0.0)
|
|
|
y = float(getattr(vector, "y", 0.0) or 0.0)
|
|
|
z = float(getattr(vector, "z", 0.0) or 0.0)
|
|
|
length = (x * x + y * y + z * z) ** 0.5
|
|
|
if length <= 1e-9:
|
|
|
return None
|
|
|
return App.Vector(x / length, y / length, z / length)
|
|
|
|
|
|
|
|
|
def _placement_with_normal_offset(placement, normal=None, offset_mm=0.0):
|
|
|
if placement is None:
|
|
|
return None
|
|
|
normal = _normalized_vector(normal)
|
|
|
if normal is None or not float(offset_mm or 0.0):
|
|
|
return placement
|
|
|
base = getattr(placement, "Base", None)
|
|
|
if base is None:
|
|
|
return placement
|
|
|
offset = float(offset_mm or 0.0)
|
|
|
moved_base = App.Vector(
|
|
|
float(getattr(base, "x", 0.0) or 0.0) + normal.x * offset,
|
|
|
float(getattr(base, "y", 0.0) or 0.0) + normal.y * offset,
|
|
|
float(getattr(base, "z", 0.0) or 0.0) + normal.z * offset,
|
|
|
)
|
|
|
return App.Placement(moved_base, getattr(placement, "Rotation", App.Rotation()))
|
|
|
|
|
|
|
|
|
def _set_device_mount_metadata(device_group, mount_target, normal=None, offset_mm=0.0):
|
|
|
if device_group is None or mount_target is None:
|
|
|
return
|
|
|
target_name = getattr(mount_target, "Name", "") or ""
|
|
|
target_label = getattr(mount_target, "Label", "") or target_name
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetMountMode",
|
|
|
"QET Mount",
|
|
|
"How this QET device was mounted in the FreeCAD scene.",
|
|
|
"manual_insert",
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetMountHostName",
|
|
|
"QET Mount",
|
|
|
"Mount target object name.",
|
|
|
target_name,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetMountHostLabel",
|
|
|
"QET Mount",
|
|
|
"Mount target object label.",
|
|
|
target_label,
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetMountHostKind",
|
|
|
"QET Mount",
|
|
|
"Mount target kind.",
|
|
|
_target_mount_kind(mount_target),
|
|
|
)
|
|
|
normal = _normalized_vector(normal)
|
|
|
if normal is not None:
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetMountHostNormalJson",
|
|
|
"QET Mount",
|
|
|
"Mount target face normal at insert time.",
|
|
|
json.dumps(_vector_payload(normal), sort_keys=True),
|
|
|
)
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetMountOffsetMm",
|
|
|
"QET Mount",
|
|
|
"Mount offset in target normal direction.",
|
|
|
"{0:.6f}".format(float(offset_mm or 0.0)),
|
|
|
)
|
|
|
|
|
|
|
|
|
def insert_pending_device(
|
|
|
doc,
|
|
|
device_group,
|
|
|
source_doc_cache=None,
|
|
|
mount_target=None,
|
|
|
mount_placement=None,
|
|
|
mount_normal=None,
|
|
|
mount_offset_mm=0.0,
|
|
|
):
|
|
|
if doc is None:
|
|
|
raise DeviceImportError("A FreeCAD document is required.")
|
|
|
device_group = _find_device_group_from_object(device_group)
|
|
|
if device_group is None:
|
|
|
raise DeviceImportError("请选择一个待装配 QET 设备。")
|
|
|
|
|
|
model_path = _native_path(getattr(device_group, "QetResolvedModelPath", ""))
|
|
|
if not model_path:
|
|
|
raise DeviceImportError("待装配设备缺少模型路径。")
|
|
|
if not os.path.isfile(model_path):
|
|
|
raise DeviceImportError("待装配设备模型文件不存在:{0}".format(model_path))
|
|
|
if not _supported_for_import(model_path):
|
|
|
raise DeviceImportError("待装配设备模型格式暂不支持:{0}".format(model_path))
|
|
|
|
|
|
existing_model_objects = _existing_model_objects(doc, device_group)
|
|
|
if existing_model_objects:
|
|
|
_set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED)
|
|
|
target_placement = mount_placement or _placement_for_mount_target(
|
|
|
mount_target,
|
|
|
getattr(getattr(device_group, "Placement", None), "Rotation", None),
|
|
|
)
|
|
|
target_placement = _placement_with_normal_offset(
|
|
|
target_placement,
|
|
|
mount_normal,
|
|
|
mount_offset_mm,
|
|
|
)
|
|
|
if target_placement is not None:
|
|
|
device_group.Placement = target_placement
|
|
|
_set_device_mount_metadata(
|
|
|
device_group,
|
|
|
mount_target,
|
|
|
normal=mount_normal,
|
|
|
offset_mm=mount_offset_mm,
|
|
|
)
|
|
|
return {
|
|
|
"device": device_group,
|
|
|
"imported_objects": existing_model_objects,
|
|
|
"already_placed": True,
|
|
|
}
|
|
|
|
|
|
_clear_group_contents(doc, device_group)
|
|
|
imported_objects = _import_model_into_group(
|
|
|
doc,
|
|
|
device_group,
|
|
|
model_path,
|
|
|
source_doc_cache=source_doc_cache if source_doc_cache is not None else {},
|
|
|
)
|
|
|
target_placement = mount_placement or _placement_for_mount_target(
|
|
|
mount_target,
|
|
|
getattr(getattr(device_group, "Placement", None), "Rotation", None),
|
|
|
)
|
|
|
target_placement = _placement_with_normal_offset(
|
|
|
target_placement,
|
|
|
mount_normal,
|
|
|
mount_offset_mm,
|
|
|
)
|
|
|
if target_placement is not None:
|
|
|
device_group.Placement = target_placement
|
|
|
_set_device_mount_metadata(
|
|
|
device_group,
|
|
|
mount_target,
|
|
|
normal=mount_normal,
|
|
|
offset_mm=mount_offset_mm,
|
|
|
)
|
|
|
_set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED)
|
|
|
try:
|
|
|
doc.recompute()
|
|
|
except Exception:
|
|
|
pass
|
|
|
return {
|
|
|
"device": device_group,
|
|
|
"imported_objects": list(imported_objects or []),
|
|
|
"already_placed": False,
|
|
|
}
|
|
|
|
|
|
|
|
|
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 _clear_group_contents(doc, group):
|
|
|
for child in list(getattr(group, "Group", []) or []):
|
|
|
child_name = getattr(child, "Name", "")
|
|
|
if child_name.startswith(TERMINAL_GROUP_PREFIX) or child_name.startswith(WIRE_GROUP_PREFIX):
|
|
|
continue
|
|
|
if getattr(child, "QetGroupKind", "").strip() in {GROUP_KIND_TERMINALS, GROUP_KIND_WIRES}:
|
|
|
continue
|
|
|
_remove_object_tree(doc, child)
|
|
|
|
|
|
|
|
|
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 _existing_model_objects(doc, group):
|
|
|
return [
|
|
|
child
|
|
|
for child in _existing_group_objects(doc, group)
|
|
|
if not _is_exchange_sidecar_group(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
|
|
|
try:
|
|
|
if TerminalObjects.is_terminal_hint_object(obj):
|
|
|
return True
|
|
|
except Exception:
|
|
|
pass
|
|
|
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,
|
|
|
source_doc_cache=None,
|
|
|
):
|
|
|
if Path(model_path).suffix.lower() == ".fcstd":
|
|
|
return _import_fcstd_into_group(
|
|
|
doc,
|
|
|
device_group,
|
|
|
model_path,
|
|
|
source_doc_cache=source_doc_cache,
|
|
|
)
|
|
|
|
|
|
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, source_doc_cache=None):
|
|
|
normalized_target = os.path.normcase(os.path.normpath(model_path))
|
|
|
if source_doc_cache is not None:
|
|
|
cached_entry = source_doc_cache.get(normalized_target)
|
|
|
if cached_entry is not None:
|
|
|
cached_doc = cached_entry.get("doc")
|
|
|
if cached_doc is not None:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _open_fcstd_source_document cache hit: name={0}, path={1}, objects={2}, should_close={3}".format(
|
|
|
getattr(cached_doc, "Name", ""),
|
|
|
getattr(cached_doc, "FileName", "") or model_path,
|
|
|
len(list(getattr(cached_doc, "Objects", []) or [])),
|
|
|
bool(cached_entry.get("should_close")),
|
|
|
)
|
|
|
)
|
|
|
return cached_doc, False
|
|
|
|
|
|
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:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _open_fcstd_source_document reusing open source doc: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(candidate, "Name", ""),
|
|
|
candidate_path,
|
|
|
len(list(getattr(candidate, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
if source_doc_cache is not None:
|
|
|
source_doc_cache[normalized_target] = {
|
|
|
"doc": candidate,
|
|
|
"should_close": False,
|
|
|
}
|
|
|
return candidate, False
|
|
|
source_doc = App.openDocument(model_path, hidden=True, temporary=True)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _open_fcstd_source_document opened temp source doc: name={0}, path={1}, objects={2}".format(
|
|
|
getattr(source_doc, "Name", "") if source_doc is not None else "<none>",
|
|
|
getattr(source_doc, "FileName", "") if source_doc is not None else model_path,
|
|
|
len(list(getattr(source_doc, "Objects", []) or [])) if source_doc is not None else -1,
|
|
|
)
|
|
|
)
|
|
|
if source_doc_cache is not None and source_doc is not None:
|
|
|
source_doc_cache[normalized_target] = {
|
|
|
"doc": source_doc,
|
|
|
"should_close": True,
|
|
|
}
|
|
|
return source_doc, False
|
|
|
return source_doc, True
|
|
|
|
|
|
|
|
|
def _import_fcstd_into_group(doc, device_group, model_path, source_doc_cache=None):
|
|
|
source_doc = None
|
|
|
should_close = False
|
|
|
try:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _import_fcstd_into_group start: target_doc={0}, target_objects={1}, device_group={2}, model_path={3}".format(
|
|
|
getattr(doc, "Name", ""),
|
|
|
len(list(getattr(doc, "Objects", []) or [])),
|
|
|
getattr(device_group, "Name", ""),
|
|
|
model_path,
|
|
|
)
|
|
|
)
|
|
|
source_doc, should_close = _open_fcstd_source_document(
|
|
|
model_path,
|
|
|
source_doc_cache=source_doc_cache,
|
|
|
)
|
|
|
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)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _import_fcstd_into_group source ready: source_doc={0}, top_level_objects={1}, should_close={2}".format(
|
|
|
getattr(source_doc, "Name", ""),
|
|
|
len(top_level_objects),
|
|
|
should_close,
|
|
|
)
|
|
|
)
|
|
|
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)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _import_fcstd_into_group completed: copied_objects={0}, direct_model_objects={1}, target_doc_objects={2}".format(
|
|
|
len(copied_objects),
|
|
|
len(direct_model_objects),
|
|
|
len(list(getattr(doc, "Objects", []) or [])),
|
|
|
)
|
|
|
)
|
|
|
return direct_model_objects
|
|
|
finally:
|
|
|
if should_close and source_doc is not None:
|
|
|
try:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _import_fcstd_into_group closing temp source doc: name={0}, path={1}".format(
|
|
|
getattr(source_doc, "Name", ""),
|
|
|
getattr(source_doc, "FileName", "") or model_path,
|
|
|
)
|
|
|
)
|
|
|
App.closeDocument(source_doc.Name)
|
|
|
except Exception:
|
|
|
pass
|
|
|
_activate_document(doc)
|
|
|
|
|
|
|
|
|
def _close_cached_source_documents(source_doc_cache, target_doc=None):
|
|
|
if not source_doc_cache:
|
|
|
return
|
|
|
|
|
|
closed_count = 0
|
|
|
for normalized_target, cached_entry in list(source_doc_cache.items()):
|
|
|
cached_doc = cached_entry.get("doc")
|
|
|
should_close = bool(cached_entry.get("should_close"))
|
|
|
if not should_close or cached_doc is None:
|
|
|
continue
|
|
|
try:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _close_cached_source_documents closing cached doc: name={0}, path={1}".format(
|
|
|
getattr(cached_doc, "Name", ""),
|
|
|
getattr(cached_doc, "FileName", "") or normalized_target,
|
|
|
)
|
|
|
)
|
|
|
App.closeDocument(cached_doc.Name)
|
|
|
closed_count += 1
|
|
|
except Exception as exc:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _close_cached_source_documents failed: name={0}, error={1}".format(
|
|
|
getattr(cached_doc, "Name", ""),
|
|
|
exc,
|
|
|
)
|
|
|
)
|
|
|
source_doc_cache.clear()
|
|
|
_append_debug_log(
|
|
|
"DeviceImport _close_cached_source_documents completed: closed_count={0}".format(
|
|
|
closed_count
|
|
|
)
|
|
|
)
|
|
|
if target_doc is not None:
|
|
|
_activate_document(target_doc)
|
|
|
|
|
|
|
|
|
def _model_index(payload):
|
|
|
index = {}
|
|
|
for item in payload.get("device_models", []):
|
|
|
instance_id = (item.get("device_instance_id") or "").strip()
|
|
|
if instance_id and instance_id not in index:
|
|
|
index[instance_id] = item
|
|
|
return index
|
|
|
|
|
|
|
|
|
def _import_cabinet_model(doc, root_group, cabinet, report, source_doc_cache=None):
|
|
|
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,
|
|
|
source_doc_cache=source_doc_cache,
|
|
|
)
|
|
|
_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="", auto_insert_pending_devices=False):
|
|
|
_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)
|
|
|
source_doc_cache = {}
|
|
|
|
|
|
report = {
|
|
|
"document_name": doc.Name,
|
|
|
"scene_path": scene_path or "",
|
|
|
"total_devices": 0,
|
|
|
"imported_devices": 0,
|
|
|
"updated_devices": 0,
|
|
|
"reused_devices": 0,
|
|
|
"added_device_details": [],
|
|
|
"updated_device_details": [],
|
|
|
"reused_device_details": [],
|
|
|
"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,
|
|
|
"pending_devices": 0,
|
|
|
"pending_device_details": [],
|
|
|
"warnings": [],
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
_import_cabinet_model(
|
|
|
doc,
|
|
|
root_group,
|
|
|
cabinet,
|
|
|
report,
|
|
|
source_doc_cache=source_doc_cache,
|
|
|
)
|
|
|
|
|
|
for index, device in enumerate(payload.get("devices", [])):
|
|
|
report["total_devices"] += 1
|
|
|
|
|
|
original_instance_id = _payload_device_instance_id(device)
|
|
|
instance_id = original_instance_id
|
|
|
element_uuid = _payload_device_element_uuid(device)
|
|
|
display_tag = (device.get("display_tag") or "").strip()
|
|
|
payload_terminal_uuids = _payload_terminal_uuid_set(device)
|
|
|
payload_terminal_signature_counts = _payload_terminal_signature_counts(device)
|
|
|
payload_terminal_entry_count = sum(payload_terminal_signature_counts.values())
|
|
|
existing_device_group = _find_device_group_by_instance_id(doc, instance_id)
|
|
|
if existing_device_group is None:
|
|
|
existing_device_group = _find_device_group(doc, element_uuid)
|
|
|
previous_display_tag = ""
|
|
|
previous_path = ""
|
|
|
existing_terminal_uuids = set()
|
|
|
existing_terminal_signature_counts = {}
|
|
|
existing_terminal_entry_count = 0
|
|
|
existing_model_objects = []
|
|
|
if existing_device_group is not None:
|
|
|
previous_display_tag = getattr(
|
|
|
existing_device_group,
|
|
|
"QetDisplayTag",
|
|
|
"",
|
|
|
).strip()
|
|
|
previous_path = getattr(
|
|
|
existing_device_group,
|
|
|
"QetResolvedModelPath",
|
|
|
"",
|
|
|
).strip()
|
|
|
existing_terminal_uuids = _existing_qet_terminal_uuids(
|
|
|
existing_device_group
|
|
|
)
|
|
|
existing_terminal_signature_counts = _existing_qet_terminal_signature_counts(
|
|
|
existing_device_group
|
|
|
)
|
|
|
existing_terminal_entry_count = sum(
|
|
|
existing_terminal_signature_counts.values()
|
|
|
)
|
|
|
existing_model_objects = _existing_model_objects(
|
|
|
doc, existing_device_group
|
|
|
)
|
|
|
model_info = models_by_element.get(instance_id, {})
|
|
|
resolved_model_path = _native_path(model_info.get("resolved_model_path", ""))
|
|
|
_append_debug_log(
|
|
|
"DeviceImport device instance_id={0}, display_tag={1}, resolved_model_path={2}".format(
|
|
|
instance_id, display_tag, resolved_model_path
|
|
|
)
|
|
|
)
|
|
|
|
|
|
if not resolved_model_path:
|
|
|
display_tag_changed = bool(
|
|
|
existing_device_group is not None
|
|
|
and previous_display_tag != display_tag
|
|
|
)
|
|
|
terminals_changed = bool(
|
|
|
existing_device_group is not None
|
|
|
and payload_terminal_signature_counts != existing_terminal_signature_counts
|
|
|
)
|
|
|
if existing_device_group is not None:
|
|
|
_update_device_group_metadata(
|
|
|
existing_device_group,
|
|
|
root_group,
|
|
|
element_uuid,
|
|
|
instance_id,
|
|
|
previous_path,
|
|
|
display_tag,
|
|
|
)
|
|
|
_store_device_payload_terminal_signatures(
|
|
|
existing_device_group,
|
|
|
payload_terminal_signature_counts,
|
|
|
)
|
|
|
if display_tag_changed:
|
|
|
change_types = ["标注"]
|
|
|
if terminals_changed:
|
|
|
change_types.append("端子")
|
|
|
report["updated_devices"] += 1
|
|
|
report["updated_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
(instance_id or getattr(existing_device_group, "QetInstanceId", "")).strip(),
|
|
|
element_uuid=element_uuid,
|
|
|
change_types=change_types,
|
|
|
previous_terminal_entry_count=existing_terminal_entry_count,
|
|
|
current_terminal_entry_count=payload_terminal_entry_count,
|
|
|
previous_display_tag=previous_display_tag,
|
|
|
previous_model_path=previous_path,
|
|
|
resolved_model_path=previous_path,
|
|
|
)
|
|
|
)
|
|
|
elif terminals_changed:
|
|
|
report["updated_devices"] += 1
|
|
|
report["updated_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
(instance_id or getattr(existing_device_group, "QetInstanceId", "")).strip(),
|
|
|
element_uuid=element_uuid,
|
|
|
change_types=["端子"],
|
|
|
added_terminal_uuids=sorted(
|
|
|
payload_terminal_uuids - existing_terminal_uuids
|
|
|
),
|
|
|
removed_terminal_uuids=sorted(
|
|
|
existing_terminal_uuids - payload_terminal_uuids
|
|
|
),
|
|
|
previous_terminal_entry_count=existing_terminal_entry_count,
|
|
|
current_terminal_entry_count=payload_terminal_entry_count,
|
|
|
previous_display_tag=previous_display_tag,
|
|
|
previous_model_path=previous_path,
|
|
|
resolved_model_path=previous_path,
|
|
|
)
|
|
|
)
|
|
|
report["skipped_missing_model"] += 1
|
|
|
report["warnings"].append(
|
|
|
"{0} 缺少 resolved_model_path,已跳过。".format(
|
|
|
_device_warning_subject(display_tag, instance_id)
|
|
|
)
|
|
|
)
|
|
|
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, instance_id),
|
|
|
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, instance_id),
|
|
|
resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if not instance_id:
|
|
|
instance_id = _generate_instance_id(
|
|
|
project_uuid, display_tag or element_uuid or "device-{0}".format(index)
|
|
|
)
|
|
|
report.setdefault("generated_instance_ids", 0)
|
|
|
report["generated_instance_ids"] += 1
|
|
|
device_group, created_now = _ensure_device_group(
|
|
|
doc,
|
|
|
root_group,
|
|
|
element_uuid,
|
|
|
instance_id,
|
|
|
resolved_model_path,
|
|
|
display_tag,
|
|
|
index,
|
|
|
)
|
|
|
|
|
|
same_source = (
|
|
|
_normalized_path_key(previous_path)
|
|
|
== _normalized_path_key(resolved_model_path)
|
|
|
)
|
|
|
added_terminal_uuids = sorted(
|
|
|
payload_terminal_uuids - existing_terminal_uuids
|
|
|
)
|
|
|
removed_terminal_uuids = sorted(
|
|
|
existing_terminal_uuids - payload_terminal_uuids
|
|
|
)
|
|
|
terminals_changed = bool(
|
|
|
added_terminal_uuids
|
|
|
or removed_terminal_uuids
|
|
|
or payload_terminal_signature_counts != existing_terminal_signature_counts
|
|
|
)
|
|
|
display_tag_changed = (
|
|
|
not created_now and previous_display_tag != display_tag
|
|
|
)
|
|
|
model_changed = (
|
|
|
not created_now
|
|
|
and (not existing_model_objects or not same_source)
|
|
|
)
|
|
|
if not auto_insert_pending_devices and not existing_model_objects:
|
|
|
_register_pending_device(
|
|
|
report,
|
|
|
device_group,
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid,
|
|
|
resolved_model_path,
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport registered pending device without importing model: instance_id={0}, model_path={1}".format(
|
|
|
instance_id,
|
|
|
resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if existing_model_objects and same_source:
|
|
|
_set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED)
|
|
|
if display_tag_changed or terminals_changed:
|
|
|
change_types = []
|
|
|
if display_tag_changed:
|
|
|
change_types.append("标注")
|
|
|
if terminals_changed:
|
|
|
change_types.append("端子")
|
|
|
report["updated_devices"] += 1
|
|
|
report["updated_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid=element_uuid,
|
|
|
change_types=change_types,
|
|
|
added_terminal_uuids=added_terminal_uuids,
|
|
|
removed_terminal_uuids=removed_terminal_uuids,
|
|
|
previous_terminal_entry_count=existing_terminal_entry_count,
|
|
|
current_terminal_entry_count=payload_terminal_entry_count,
|
|
|
previous_display_tag=previous_display_tag,
|
|
|
previous_model_path=previous_path,
|
|
|
resolved_model_path=resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport import skipped: metadata-only change for instance_id={0}, display_tag_changed={1}, added_terminals={2}, removed_terminals={3}".format(
|
|
|
instance_id,
|
|
|
display_tag_changed,
|
|
|
len(added_terminal_uuids),
|
|
|
len(removed_terminal_uuids),
|
|
|
)
|
|
|
)
|
|
|
_store_device_payload_terminal_signatures(
|
|
|
device_group,
|
|
|
payload_terminal_signature_counts,
|
|
|
)
|
|
|
continue
|
|
|
report["reused_devices"] += 1
|
|
|
report["reused_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid=element_uuid,
|
|
|
previous_terminal_entry_count=existing_terminal_entry_count,
|
|
|
current_terminal_entry_count=payload_terminal_entry_count,
|
|
|
previous_display_tag=previous_display_tag,
|
|
|
previous_model_path=previous_path,
|
|
|
resolved_model_path=resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport import skipped: reused existing device group for instance_id={0}, model_path={1}, existing_model_objects={2}".format(
|
|
|
instance_id,
|
|
|
resolved_model_path,
|
|
|
len(existing_model_objects),
|
|
|
)
|
|
|
)
|
|
|
_store_device_payload_terminal_signatures(
|
|
|
device_group,
|
|
|
payload_terminal_signature_counts,
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if created_now or not existing_model_objects:
|
|
|
_clear_group_contents(doc, device_group)
|
|
|
|
|
|
try:
|
|
|
_append_debug_log(
|
|
|
"DeviceImport importing model for device_instance_id={0}: {1}".format(
|
|
|
instance_id, resolved_model_path
|
|
|
)
|
|
|
)
|
|
|
_import_model_into_group(
|
|
|
doc,
|
|
|
device_group,
|
|
|
resolved_model_path,
|
|
|
source_doc_cache=source_doc_cache,
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport import succeeded for device_instance_id={0}".format(
|
|
|
instance_id
|
|
|
)
|
|
|
)
|
|
|
if existing_model_objects:
|
|
|
_remove_model_objects(doc, existing_model_objects)
|
|
|
_set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED)
|
|
|
except Exception as exc:
|
|
|
if existing_model_objects:
|
|
|
_ensure_string_property(
|
|
|
device_group,
|
|
|
"QetResolvedModelPath",
|
|
|
"QET Exchange",
|
|
|
"Resolved local model path from QET exchange",
|
|
|
previous_path,
|
|
|
)
|
|
|
report["skipped_import_error"] += 1
|
|
|
report["warnings"].append(
|
|
|
"{0} 导入失败:{1}".format(
|
|
|
_device_warning_subject(display_tag, element_uuid or instance_id),
|
|
|
exc,
|
|
|
)
|
|
|
)
|
|
|
_append_debug_log(
|
|
|
"DeviceImport import failed for device_instance_id={0}: {1}".format(
|
|
|
instance_id, exc
|
|
|
)
|
|
|
)
|
|
|
continue
|
|
|
|
|
|
if created_now:
|
|
|
report["imported_devices"] += 1
|
|
|
report["added_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid=element_uuid,
|
|
|
previous_terminal_entry_count=existing_terminal_entry_count,
|
|
|
current_terminal_entry_count=payload_terminal_entry_count,
|
|
|
previous_display_tag=previous_display_tag,
|
|
|
previous_model_path=previous_path,
|
|
|
resolved_model_path=resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
else:
|
|
|
report["updated_devices"] += 1
|
|
|
change_types = []
|
|
|
if display_tag_changed:
|
|
|
change_types.append("标注")
|
|
|
if model_changed:
|
|
|
change_types.append("3D模型")
|
|
|
if terminals_changed:
|
|
|
change_types.append("端子")
|
|
|
if not change_types:
|
|
|
change_types.append("3D模型")
|
|
|
report["updated_device_details"].append(
|
|
|
_device_change_detail(
|
|
|
display_tag,
|
|
|
instance_id,
|
|
|
element_uuid=element_uuid,
|
|
|
change_types=change_types,
|
|
|
added_terminal_uuids=added_terminal_uuids,
|
|
|
removed_terminal_uuids=removed_terminal_uuids,
|
|
|
previous_terminal_entry_count=existing_terminal_entry_count,
|
|
|
current_terminal_entry_count=payload_terminal_entry_count,
|
|
|
previous_display_tag=previous_display_tag,
|
|
|
previous_model_path=previous_path,
|
|
|
resolved_model_path=resolved_model_path,
|
|
|
)
|
|
|
)
|
|
|
|
|
|
_store_device_payload_terminal_signatures(
|
|
|
device_group,
|
|
|
payload_terminal_signature_counts,
|
|
|
)
|
|
|
|
|
|
if not original_instance_id:
|
|
|
report["imported_without_instance_id"] += 1
|
|
|
finally:
|
|
|
_close_cached_source_documents(source_doc_cache, target_doc=doc)
|
|
|
|
|
|
TerminalObjects.sort_group_children(root_group)
|
|
|
doc.recompute()
|
|
|
_append_debug_log("DeviceImport ViewFit skipped during exchange import")
|
|
|
|
|
|
_append_debug_log(
|
|
|
"DeviceImport finished: cabinet_imported={0}, imported={1}, updated={2}, reused={3}, skipped_missing_model={4}, skipped_missing_file={5}, skipped_import_error={6}".format(
|
|
|
report["cabinet_imported"],
|
|
|
report["imported_devices"],
|
|
|
report["updated_devices"],
|
|
|
report["reused_devices"],
|
|
|
report["skipped_missing_model"],
|
|
|
report["skipped_missing_file"],
|
|
|
report["skipped_import_error"],
|
|
|
)
|
|
|
)
|
|
|
return report
|
|
|
|
|
|
|
|
|
class CommandInsertPendingDevice:
|
|
|
def GetResources(self):
|
|
|
return {
|
|
|
"MenuText": "插入待装配设备",
|
|
|
"ToolTip": "将选中的 QET 待装配设备模型插入当前 3D 场景",
|
|
|
}
|
|
|
|
|
|
def IsActive(self):
|
|
|
return getattr(App, "ActiveDocument", None) is not None and Gui is not None
|
|
|
|
|
|
def Activated(self):
|
|
|
if Gui is None:
|
|
|
return
|
|
|
selection = list(Gui.Selection.getSelection() or [])
|
|
|
device_group = None
|
|
|
for obj in selection:
|
|
|
device_group = _find_device_group_from_object(obj)
|
|
|
if device_group is not None:
|
|
|
break
|
|
|
if device_group is None:
|
|
|
try:
|
|
|
App.Console.PrintWarning("[FreeCADExchange] 请先选择一个待装配 QET 设备。\n")
|
|
|
except Exception:
|
|
|
pass
|
|
|
return
|
|
|
try:
|
|
|
result = insert_pending_device(App.ActiveDocument, device_group)
|
|
|
try:
|
|
|
App.Console.PrintMessage(
|
|
|
"[FreeCADExchange] 已插入设备:{0},导入对象 {1} 个。\n".format(
|
|
|
getattr(result["device"], "Label", ""),
|
|
|
len(result.get("imported_objects", []) or []),
|
|
|
)
|
|
|
)
|
|
|
except Exception:
|
|
|
pass
|
|
|
try:
|
|
|
Gui.SendMsgToActiveView("ViewFit")
|
|
|
except Exception:
|
|
|
pass
|
|
|
except Exception as exc:
|
|
|
try:
|
|
|
App.Console.PrintError("[FreeCADExchange] 插入待装配设备失败:{0}\n".format(exc))
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
|
|
|
_COMMANDS_REGISTERED = False
|
|
|
|
|
|
|
|
|
def register_commands():
|
|
|
global _COMMANDS_REGISTERED
|
|
|
if _COMMANDS_REGISTERED:
|
|
|
return
|
|
|
if Gui is None or not hasattr(Gui, "addCommand"):
|
|
|
return
|
|
|
try:
|
|
|
Gui.addCommand("QET_Exchange_InsertPendingDevice", CommandInsertPendingDevice())
|
|
|
_COMMANDS_REGISTERED = True
|
|
|
except Exception as exc:
|
|
|
_append_debug_log("failed to register pending device command: {0}".format(exc))
|