# FreeCADExchange FCStd template instantiation helpers. import os from pathlib import Path import uuid import FreeCAD as App try: import FreeCADGui as Gui except ImportError: Gui = None try: from PySide6 import QtWidgets except ImportError: try: from PySide2 import QtWidgets except ImportError: try: from PySide import QtGui as QtWidgets except ImportError: QtWidgets = None import DeviceImport import TemplateSemantics import TerminalObjects COMMAND_IMPORT_TEMPLATE_INSTANCE = "QET_Template_ImportInstance" COMMAND_CREATE_ENGINEERING_TERMINALS = "QET_Template_CreateEngineeringTerminals" class TemplateInstantiationError(RuntimeError): pass def _debug(message): try: DeviceImport._append_debug_log(message) except Exception: pass def _project_uuid_for_document(doc): root = TerminalObjects.ensure_root_group(doc) project_uuid = getattr(root, "QetProjectUuid", "").strip() if project_uuid: return project_uuid project_uuid = "local-project" TerminalObjects.ensure_string_property( root, "QetProjectUuid", "QET Exchange", "Project UUID for the exchange document", project_uuid, ) return project_uuid def _existing_terminal_by_slot(terminal_group): result = {} for obj in TerminalObjects.collect_terminal_objects(terminal_group): slot_name = getattr(obj, "QetTemplateSlotName", "").strip() if slot_name and slot_name not in result: result[slot_name] = obj return result def _is_qet_bound_terminal(obj): terminal_uuid = getattr(obj, "QetTerminalUuid", "").strip() binding_mode = getattr(obj, "QetTerminalBindingMode", "").strip().lower() if not terminal_uuid: return False if TerminalObjects.is_local_terminal_uuid(terminal_uuid): return False if binding_mode == TerminalObjects.TERMINAL_BINDING_MODE_LOCAL: return False return True def _terminal_group_for_device(doc, device_group, project_uuid): instance_id = getattr(device_group, "QetInstanceId", "").strip() return TerminalObjects.ensure_terminal_group( doc, device_group, project_uuid=project_uuid, instance_id=instance_id, ) def _slot_label(slot, fallback): return (slot.get("label") or slot.get("name") or fallback or "QET Terminal").strip() def _slot_placement(slot): base = slot.get("base") if not isinstance(base, App.Vector): base = App.Vector(0, 0, 0) rotation = App.Rotation() rotation_value = slot.get("rotation") if isinstance(rotation_value, dict): axis = rotation_value.get("axis") angle = rotation_value.get("angle") if isinstance(axis, App.Vector) and angle is not None: try: rotation = App.Rotation(axis, float(angle)) except Exception: rotation = App.Rotation() return App.Placement(base, rotation) def _iter_device_groups(doc): result = [] for obj in list(getattr(doc, "Objects", []) or []): if getattr(obj, "Name", "").startswith(DeviceImport.DEVICE_GROUP_PREFIX): if "QetElementUuid" in getattr(obj, "PropertiesList", []): result.append(obj) return result def ensure_engineering_terminals_for_device(doc, device_group): if doc is None: raise TemplateInstantiationError("A FreeCAD document is required.") if device_group is None: raise TemplateInstantiationError("A device group is required.") project_uuid = ( getattr(device_group, "QetProjectUuid", "").strip() or _project_uuid_for_document(doc) ) element_uuid = getattr(device_group, "QetElementUuid", "").strip() instance_id = getattr(device_group, "QetInstanceId", "").strip() model_path = getattr(device_group, "QetResolvedModelPath", "").strip() slots = TemplateSemantics.collect_terminal_hints(device_group) terminal_group = _terminal_group_for_device(doc, device_group, project_uuid) existing_by_slot = _existing_terminal_by_slot(terminal_group) report = { "device": getattr(device_group, "Name", ""), "slots": len(slots), "created_terminals": 0, "updated_terminals": 0, "skipped_slots": 0, "skipped_unbound_slots": 0, "skipped_devices_without_template_slots": 0, "local_terminals": 0, "warnings": [], } if not slots: report["skipped_devices_without_template_slots"] = 1 report["warnings"].append( "设备 {0} 没有模板端子,未生成工程端子。请先制作带模板端子的 FCStd。".format( getattr(device_group, "Label", "") or getattr(device_group, "Name", "") ) ) _debug(report["warnings"][-1]) return report for index, slot in enumerate(slots): slot_name = (slot.get("name") or "SLOT_{0}".format(index + 1)).strip() if not slot_name: report["skipped_slots"] += 1 continue terminal_obj = existing_by_slot.get(slot_name) if terminal_obj is None: report["skipped_unbound_slots"] += 1 report["warnings"].append( "设备 {0} 的模板槽位 {1} 没有 QET 端子绑定,未生成本地工程端子。".format( getattr(device_group, "Label", "") or getattr(device_group, "Name", ""), slot_name, ) ) _debug(report["warnings"][-1]) continue if not _is_qet_bound_terminal(terminal_obj): report["local_terminals"] += 1 report["warnings"].append( "设备 {0} 的模板槽位 {1} 已存在本地端子,已保留但不作为 QET 工程端子更新。".format( getattr(device_group, "Label", "") or getattr(device_group, "Name", ""), slot_name, ) ) _debug(report["warnings"][-1]) continue else: try: terminal_obj.Placement = _slot_placement(slot) except Exception: pass report["updated_terminals"] += 1 terminal_uuid = getattr(terminal_obj, "QetTerminalUuid", "").strip() TerminalObjects.set_terminal_semantics( terminal_obj, project_uuid, element_uuid, terminal_uuid, instance_id, label=_slot_label(slot, slot_name), slot_name=slot_name, ) try: terminal_obj.ViewObject.Visibility = True terminal_obj.ViewObject.ShapeColor = (0.0, 0.75, 1.0) except Exception: pass source_obj = slot.get("source_object") if source_obj is not None and source_obj is not terminal_obj: try: source_obj.ViewObject.Visibility = False except Exception: pass try: doc.recompute() except Exception: pass _debug( "TemplateInstantiation terminals: device={0}, model={1}, slots={2}, created={3}, updated={4}".format( getattr(device_group, "Name", ""), model_path, report["slots"], report["created_terminals"], report["updated_terminals"], ) ) return report def ensure_engineering_terminals_for_selection_or_all(doc=None): doc = doc or getattr(App, "ActiveDocument", None) if doc is None: raise TemplateInstantiationError("请先打开 FreeCAD 工程。") selected = [] if Gui is not None and hasattr(Gui, "Selection"): try: selected = list(Gui.Selection.getSelection()) or [] except Exception: selected = [] devices = [ obj for obj in selected if getattr(obj, "Name", "").startswith(DeviceImport.DEVICE_GROUP_PREFIX) ] if not devices: devices = _iter_device_groups(doc) if not devices: raise TemplateInstantiationError("没有找到 QET 设备实例。") reports = [ensure_engineering_terminals_for_device(doc, device) for device in devices] return { "devices": len(reports), "created_terminals": sum(item["created_terminals"] for item in reports), "updated_terminals": sum(item["updated_terminals"] for item in reports), "skipped_devices_without_template_slots": sum( item.get("skipped_devices_without_template_slots", 0) for item in reports ), "skipped_unbound_slots": sum(item.get("skipped_unbound_slots", 0) for item in reports), "local_terminals": sum(item.get("local_terminals", 0) for item in reports), "warnings": [ warning for item in reports for warning in list(item.get("warnings", []) or []) ], "reports": reports, } def create_device_instance_from_template(doc, model_path, label=""): if doc is None: raise TemplateInstantiationError("A FreeCAD document is required.") model_path = os.path.normpath(os.path.expanduser(os.path.expandvars(model_path or ""))) if not model_path: raise TemplateInstantiationError("请选择 FCStd 设备模板。") if not os.path.isfile(model_path): raise TemplateInstantiationError("FCStd 设备模板不存在:{0}".format(model_path)) if Path(model_path).suffix.lower() != ".fcstd": raise TemplateInstantiationError("请选择 .FCStd 设备模板。") project_uuid = _project_uuid_for_document(doc) root = TerminalObjects.ensure_root_group(doc, project_uuid) element_uuid = str(uuid.uuid4()) instance_id = str(uuid.uuid4()) display_label = (label or "").strip() or Path(model_path).stem device_group, _created = DeviceImport._ensure_device_group( doc, root, element_uuid, instance_id, model_path, display_label, 0, ) DeviceImport._clear_group_contents(doc, device_group) DeviceImport._import_model_into_group(doc, device_group, model_path) report = ensure_engineering_terminals_for_device(doc, device_group) try: doc.recompute() except Exception: pass return { "device": device_group, "element_uuid": element_uuid, "instance_id": instance_id, "terminal_report": report, } class CommandImportTemplateInstance: def GetResources(self): return { "MenuText": "导入模板实例", "ToolTip": "把 FCStd 设备模板作为工程设备实例导入当前场景", } def IsActive(self): return getattr(App, "ActiveDocument", None) is not None and Gui is not None def Activated(self): if QtWidgets is None: try: App.Console.PrintError("[FreeCADExchange] Qt file dialog is not available.\n") except Exception: pass return file_path, _selected_filter = QtWidgets.QFileDialog.getOpenFileName( None, "选择 FCStd 设备模板", "", "FreeCAD template (*.FCStd *.fcstd);;All files (*.*)", ) if not file_path: return try: result = create_device_instance_from_template(App.ActiveDocument, file_path) try: App.Console.PrintMessage( "[FreeCADExchange] 已导入设备实例:{0},端子新增 {1} 个。\n".format( getattr(result["device"], "Label", ""), result["terminal_report"]["created_terminals"], ) ) except Exception: pass except Exception as exc: try: App.Console.PrintError("[FreeCADExchange] 导入模板实例失败:{0}\n".format(exc)) except Exception: pass class CommandCreateEngineeringTerminals: def GetResources(self): return { "MenuText": "生成工程端子", "ToolTip": "把设备模板 LCS 槽位转换为可布线工程端子", } def IsActive(self): return getattr(App, "ActiveDocument", None) is not None and Gui is not None def Activated(self): try: report = ensure_engineering_terminals_for_selection_or_all(App.ActiveDocument) try: App.Console.PrintMessage( "[FreeCADExchange] 工程端子生成完成:设备 {0} 个,新增 {1} 个,更新 {2} 个,跳过未绑定槽位 {3} 个,已有本地端子 {4} 个,跳过无模板设备 {5} 个。\n".format( report["devices"], report["created_terminals"], report["updated_terminals"], report["skipped_unbound_slots"], report["local_terminals"], report["skipped_devices_without_template_slots"], ) ) for warning in list(report.get("warnings", []) or []): App.Console.PrintWarning("[FreeCADExchange] {0}\n".format(warning)) 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: return try: Gui.addCommand(COMMAND_IMPORT_TEMPLATE_INSTANCE, CommandImportTemplateInstance()) Gui.addCommand(COMMAND_CREATE_ENGINEERING_TERMINALS, CommandCreateEngineeringTerminals()) _COMMANDS_REGISTERED = True except Exception as exc: _debug("failed to register template instantiation commands: {0}".format(exc))