You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
14 KiB
Python

# 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))