feature/FreeCAD工程端子与手动布线-zwl-0521

dev
Zhaowenlong 6 days ago
parent 6b8ffb4036
commit 14f7bae6bd

@ -9,6 +9,7 @@ set(FreeCADExchange_Scripts
TemplateSemantics.py TemplateSemantics.py
TemplateAuthoring.py TemplateAuthoring.py
TemplateAuthoringPanel.py TemplateAuthoringPanel.py
TemplateInstantiation.py
TerminalImport.py TerminalImport.py
ExchangeWriteBack.py ExchangeWriteBack.py
ManualWiring.py ManualWiring.py

@ -12,6 +12,9 @@ COMMANDS = [
"QET_Template_AddTerminal", "QET_Template_AddTerminal",
"QET_Template_ValidateTerminals", "QET_Template_ValidateTerminals",
"QET_Template_SaveAsFCStd", "QET_Template_SaveAsFCStd",
"QET_Template_ImportInstance",
"QET_Template_CreateEngineeringTerminals",
"QET_Exchange_CreateManualWire",
] ]
@ -62,6 +65,7 @@ def _register_exchange_commands(
manual_wiring = safe_import("ManualWiring") manual_wiring = safe_import("ManualWiring")
template_authoring = safe_import("TemplateAuthoring") template_authoring = safe_import("TemplateAuthoring")
template_authoring_panel = safe_import("TemplateAuthoringPanel") template_authoring_panel = safe_import("TemplateAuthoringPanel")
template_instantiation = safe_import("TemplateInstantiation")
try: try:
if exchange_write_back is not None: if exchange_write_back is not None:
@ -113,6 +117,16 @@ def _register_exchange_commands(
) )
) )
try:
if template_instantiation is not None:
template_instantiation.register_commands()
except Exception:
append_init_log(
"InitGui failed to register template instantiation commands:\n{0}".format(
traceback_module.format_exc()
)
)
def _bootstrap_if_requested( def _bootstrap_if_requested(
safe_import=_safe_import, safe_import=_safe_import,

@ -177,8 +177,8 @@ def create_manual_wire(doc, start_terminal, end_terminal, waypoints=None, parent
class CommandCreateManualWire: class CommandCreateManualWire:
def GetResources(self): def GetResources(self):
return { return {
"MenuText": "Create Manual Wire", "MenuText": "连接选中端子",
"ToolTip": "Create a manual wire between two selected terminals", "ToolTip": "在两个选中的工程端子之间创建手动导线",
} }
def IsActive(self): def IsActive(self):

@ -235,6 +235,7 @@ class CommandOpenTemplateAuthoringPanel:
_COMMANDS_REGISTERED = False _COMMANDS_REGISTERED = False
_MENU_ACTION_INSTALLED = False _MENU_ACTION_INSTALLED = False
_TOOLBAR_ACTION_INSTALLED = False _TOOLBAR_ACTION_INSTALLED = False
INSTALL_GLOBAL_ACTIONS = False
def _tools_menu(): def _tools_menu():
@ -320,6 +321,8 @@ def register_commands():
if not _COMMANDS_REGISTERED: if not _COMMANDS_REGISTERED:
Gui.addCommand(COMMAND_NAME, CommandOpenTemplateAuthoringPanel()) Gui.addCommand(COMMAND_NAME, CommandOpenTemplateAuthoringPanel())
_COMMANDS_REGISTERED = True _COMMANDS_REGISTERED = True
if not INSTALL_GLOBAL_ACTIONS:
return
try: try:
install_menu_action() install_menu_action()
except RuntimeError as exc: except RuntimeError as exc:

@ -0,0 +1,370 @@
# 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 _local_terminal_uuid(instance_id, element_uuid, slot_name):
owner = (instance_id or "").strip() or (element_uuid or "").strip() or "device"
slot = (slot_name or "").strip() or "slot"
return "local:{0}:{1}".format(owner, slot)
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 _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,
}
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:
terminal_obj = TerminalObjects.create_lcs_object(
doc,
"QETTerminal_{0}_{1}".format(
TerminalObjects.safe_token(instance_id or element_uuid),
TerminalObjects.safe_token(slot_name),
),
placement=_slot_placement(slot),
label=_slot_label(slot, slot_name),
)
terminal_group.addObject(terminal_obj)
report["created_terminals"] += 1
else:
try:
terminal_obj.Placement = _slot_placement(slot)
except Exception:
pass
report["updated_terminals"] += 1
terminal_uuid = getattr(terminal_obj, "QetTerminalUuid", "").strip()
if not terminal_uuid:
terminal_uuid = _local_terminal_uuid(instance_id, element_uuid, slot_name)
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),
"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} 个。\n".format(
report["devices"],
report["created_terminals"],
report["updated_terminals"],
)
)
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))

@ -186,9 +186,13 @@ def collect_terminal_hints(container):
for child in list(getattr(container, "Group", []) or []): for child in list(getattr(container, "Group", []) or []):
if TerminalObjects.is_terminal_hint_object(child) and not TerminalObjects.is_terminal_object(child): if TerminalObjects.is_terminal_hint_object(child) and not TerminalObjects.is_terminal_object(child):
slot_name = (
getattr(child, "QetTemplateSlotName", "")
or getattr(child, "Name", "")
)
hint = _slot_from_payload( hint = _slot_from_payload(
{ {
"name": getattr(child, "Name", ""), "name": slot_name,
"label": getattr(child, "Label", "") or getattr(child, "Name", ""), "label": getattr(child, "Label", "") or getattr(child, "Name", ""),
"base": [ "base": [
getattr(child.Placement.Base, "x", 0.0), getattr(child.Placement.Base, "x", 0.0),

@ -259,10 +259,53 @@ def is_terminal_object(obj):
def terminal_origin(obj): def terminal_origin(obj):
try:
if hasattr(obj, "getGlobalPlacement"):
placement = obj.getGlobalPlacement()
base = getattr(placement, "Base", None)
if base is not None:
return App.Vector(base.x, base.y, base.z)
except Exception:
pass
try: try:
placement = getattr(obj, "Placement", None) placement = getattr(obj, "Placement", None)
if placement is not None: if placement is not None:
return App.Vector(placement.Base.x, placement.Base.y, placement.Base.z) local_point = App.Vector(placement.Base.x, placement.Base.y, placement.Base.z)
parent_chain = []
current = obj
visited = set()
while True:
parents = list(getattr(current, "InList", []) or [])
parent = None
for candidate in parents:
if id(candidate) in visited:
continue
if getattr(candidate, "Placement", None) is not None:
parent = candidate
break
if parent is None:
break
visited.add(id(parent))
parent_chain.append(parent)
current = parent
point = local_point
for parent in parent_chain:
parent_placement = getattr(parent, "Placement", None)
if parent_placement is None:
continue
try:
point = parent_placement.multVec(point)
except Exception:
base = getattr(parent_placement, "Base", None)
if base is not None:
point = App.Vector(
point.x + base.x,
point.y + base.y,
point.z + base.z,
)
return point
except Exception: except Exception:
pass pass
return App.Vector(0, 0, 0) return App.Vector(0, 0, 0)

@ -24,9 +24,16 @@ def _install_fake_freecad():
class Placement: class Placement:
def __init__(self, base=None, rotation=None): def __init__(self, base=None, rotation=None):
self.Base = base self.Base = base or Vector()
self.Rotation = rotation self.Rotation = rotation
def multVec(self, vector):
return Vector(
self.Base.x + vector.x,
self.Base.y + vector.y,
self.Base.z + vector.z,
)
fake_freecad = types.ModuleType("FreeCAD") fake_freecad = types.ModuleType("FreeCAD")
fake_freecad.Vector = Vector fake_freecad.Vector = Vector
fake_freecad.Rotation = Rotation fake_freecad.Rotation = Rotation
@ -70,6 +77,8 @@ class FakeObject:
self.Group = [] self.Group = []
self.ViewObject = FakeViewObject() self.ViewObject = FakeViewObject()
self.Shape = None self.Shape = None
self.Placement = sys.modules["FreeCAD"].Placement()
self.InList = []
def isDerivedFrom(self, type_name): def isDerivedFrom(self, type_name):
if self.TypeId == type_name: if self.TypeId == type_name:
@ -87,6 +96,8 @@ class FakeObject:
def addObject(self, child): def addObject(self, child):
if child not in self.Group: if child not in self.Group:
self.Group.append(child) self.Group.append(child)
if self not in child.InList:
child.InList.append(self)
class FakeDocument: class FakeDocument:
@ -127,6 +138,68 @@ def _reload_modules():
class ManualWiringGroupTest(unittest.TestCase): class ManualWiringGroupTest(unittest.TestCase):
def test_manual_wire_uses_terminal_global_points_after_device_move(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
root = terminal_objects.ensure_root_group(doc, "project-1")
start_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_start")
start_device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation())
root.addObject(start_device)
end_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_end")
end_device.Placement = app.Placement(app.Vector(300, 0, 0), app.Rotation())
root.addObject(end_device)
for device, element_uuid, instance_id in [
(start_device, "device-start", "instance-start"),
(end_device, "device-end", "instance-end"),
]:
terminal_objects.ensure_string_property(
device,
"QetElementUuid",
"QET Exchange",
"Element UUID",
element_uuid,
)
terminal_objects.ensure_string_property(
device,
"QetInstanceId",
"QET Exchange",
"Instance ID",
instance_id,
)
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation())
start_device.addObject(start_terminal)
terminal_objects.set_terminal_semantics(
start_terminal,
"project-1",
"device-start",
"terminal-start",
"instance-start",
label="Start",
)
end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd")
end_terminal.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation())
end_device.addObject(end_terminal)
terminal_objects.set_terminal_semantics(
end_terminal,
"project-1",
"device-end",
"terminal-end",
"instance-end",
label="End",
)
wire = manual_wiring.create_manual_wire(doc, start_terminal, end_terminal)
self.assertEqual(110.0, wire.Shape[0].x)
self.assertEqual(320.0, wire.Shape[-1].x)
def test_manual_wire_is_added_to_device_wire_group(self): def test_manual_wire_is_added_to_device_wire_group(self):
_install_fake_freecad() _install_fake_freecad()
device_import, manual_wiring, terminal_objects = _reload_modules() device_import, manual_wiring, terminal_objects = _reload_modules()

@ -49,16 +49,16 @@ def _reload_panel_module():
class TemplateAuthoringPanelTest(unittest.TestCase): class TemplateAuthoringPanelTest(unittest.TestCase):
def test_register_commands_ignores_menu_install_runtime_errors(self): def test_register_commands_does_not_touch_global_menu_or_toolbar(self):
_install_fake_modules() _install_fake_modules()
panel_module = _reload_panel_module() panel_module = _reload_panel_module()
panel_module._COMMANDS_REGISTERED = False panel_module._COMMANDS_REGISTERED = False
def raise_deleted_menu_error(): def fail_if_called():
raise RuntimeError("Internal C++ object already deleted") raise AssertionError("global menu/toolbar installation should not run")
panel_module.install_menu_action = raise_deleted_menu_error panel_module.install_menu_action = fail_if_called
panel_module.install_toolbar_action = raise_deleted_menu_error panel_module.install_toolbar_action = fail_if_called
panel_module.register_commands() panel_module.register_commands()

@ -0,0 +1,168 @@
import importlib
import sys
import types
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange"
if str(MODULE_DIR) not in sys.path:
sys.path.insert(0, str(MODULE_DIR))
def _install_fake_freecad():
class Vector:
def __init__(self, x=0.0, y=0.0, z=0.0):
self.x = float(x)
self.y = float(y)
self.z = float(z)
class Rotation:
def __init__(self, axis=None, angle=None):
self.Axis = axis
self.Angle = angle
class Placement:
def __init__(self, base=None, rotation=None):
self.Base = base or Vector()
self.Rotation = rotation or Rotation()
fake_freecad = types.ModuleType("FreeCAD")
fake_freecad.Vector = Vector
fake_freecad.Rotation = Rotation
fake_freecad.Placement = Placement
fake_freecad.Console = types.SimpleNamespace(
PrintMessage=lambda *args, **kwargs: None,
PrintWarning=lambda *args, **kwargs: None,
PrintError=lambda *args, **kwargs: None,
)
fake_freecad.ActiveDocument = None
sys.modules["FreeCAD"] = fake_freecad
fake_freecadgui = types.ModuleType("FreeCADGui")
fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None
fake_freecadgui.addCommand = lambda *args, **kwargs: None
fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: [])
sys.modules["FreeCADGui"] = fake_freecadgui
fake_importgui = types.ModuleType("ImportGui")
fake_importgui.insert = lambda *args, **kwargs: None
sys.modules["ImportGui"] = fake_importgui
class FakeViewObject:
def __init__(self):
self.Visibility = True
self.ShapeColor = None
class FakeObject:
def __init__(self, name, type_id):
self.Name = name
self.Label = name
self.TypeId = type_id
self.PropertiesList = []
self.Group = []
self.InList = []
self.ViewObject = FakeViewObject()
self.Placement = sys.modules["FreeCAD"].Placement()
def isDerivedFrom(self, type_name):
if self.TypeId == type_name:
return True
if type_name == "App::DocumentObjectGroup":
return self.TypeId in {"App::DocumentObjectGroup", "App::Part"}
if type_name == "App::LocalCoordinateSystem":
return self.TypeId in {"Part::LocalCoordinateSystem", "PartDesign::CoordinateSystem"}
return False
def addProperty(self, prop_type, prop_name, group_name, description):
if prop_name not in self.PropertiesList:
self.PropertiesList.append(prop_name)
def addObject(self, child):
if child not in self.Group:
self.Group.append(child)
if self not in child.InList:
child.InList.append(self)
class FakeDocument:
def __init__(self):
self.Name = "QETScene"
self.Objects = []
def addObject(self, type_name, name):
obj = FakeObject(name, type_name)
self.Objects.append(obj)
return obj
def getObject(self, name):
for obj in self.Objects:
if obj.Name == name:
return obj
return None
def removeObject(self, name):
self.Objects = [obj for obj in self.Objects if obj.Name != name]
def recompute(self):
return None
def _reload_modules():
for name in [
"TerminalObjects",
"TemplateSemantics",
"DeviceImport",
"TemplateInstantiation",
]:
sys.modules.pop(name, None)
return importlib.import_module("TemplateInstantiation"), importlib.import_module("TerminalObjects")
class TemplateInstantiationTest(unittest.TestCase):
def test_template_lcs_slots_become_engineering_terminals(self):
_install_fake_freecad()
template_instantiation, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
root = terminal_objects.ensure_root_group(doc, "project-1")
device = doc.addObject("App::Part", "QETDevice_ct_1")
root.addObject(device)
terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "ct-1")
terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "inst-1")
terminal_objects.ensure_string_property(device, "QetProjectUuid", "QET Exchange", "", "project-1")
p1 = doc.addObject("Part::LocalCoordinateSystem", "P1")
p1.Placement = app.Placement(app.Vector(10, 20, 30), app.Rotation())
p1.addProperty("App::PropertyString", "Role", "QET Template", "")
p1.Role = "Terminal"
p1.addProperty("App::PropertyBool", "CanWire", "QET Template", "")
p1.CanWire = True
p1.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
p1.QetTemplateSlotName = "P1"
device.addObject(p1)
report = template_instantiation.ensure_engineering_terminals_for_device(doc, device)
terminal_group = terminal_objects.find_child_group_by_kind(
device,
terminal_objects.TERMINAL_GROUP_KIND,
)
terminals = terminal_objects.collect_terminal_objects(terminal_group)
self.assertEqual(1, report["created_terminals"])
self.assertEqual(1, len(terminals))
self.assertEqual("local:inst-1:P1", terminals[0].QetTerminalUuid)
self.assertEqual("inst-1", terminals[0].QetInstanceId)
self.assertEqual("ct-1", terminals[0].QetElementUuid)
self.assertEqual("P1", terminals[0].QetTemplateSlotName)
self.assertTrue(terminals[0].CanWire)
self.assertFalse(p1.ViewObject.Visibility)
if __name__ == "__main__":
unittest.main()

@ -68,6 +68,27 @@ def _reload_exchange_modules():
class TemplateSemanticsRotationTest(unittest.TestCase): class TemplateSemanticsRotationTest(unittest.TestCase):
def test_terminal_hint_uses_template_slot_name_before_object_name(self):
_install_fake_freecad()
template_semantics, _ = _reload_exchange_modules()
fake_lcs = types.SimpleNamespace(
Name="Terminal_P1",
Label="端子一",
TypeId="Part::LocalCoordinateSystem",
Role="Terminal",
QetTemplateSlotName="P1",
Placement=types.SimpleNamespace(
Base=sys.modules["FreeCAD"].Vector(1, 2, 3),
Rotation=sys.modules["FreeCAD"].Rotation(),
),
)
container = types.SimpleNamespace(Group=[fake_lcs])
hints = template_semantics.collect_terminal_hints(container)
self.assertEqual("P1", hints[0]["name"])
def test_terminal_hint_keeps_source_object_rotation(self): def test_terminal_hint_keeps_source_object_rotation(self):
_install_fake_freecad() _install_fake_freecad()
template_semantics, _ = _reload_exchange_modules() template_semantics, _ = _reload_exchange_modules()

Loading…
Cancel
Save