From 2dba711bb3ab213c3a33354a9d4f04d462a63a36 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Thu, 28 May 2026 16:09:30 +0800 Subject: [PATCH] fix: stop creating local qet terminals --- src/Mod/FreeCADExchange/ManualWiringPanel.py | 35 +++++++++- .../FreeCADExchange/TemplateInstantiation.py | 56 ++++++++++------ ...eecad_exchange_manual_wiring_panel_test.py | 24 +++++++ ...ad_exchange_template_instantiation_test.py | 65 ++++++++++++++++--- 4 files changed, 148 insertions(+), 32 deletions(-) diff --git a/src/Mod/FreeCADExchange/ManualWiringPanel.py b/src/Mod/FreeCADExchange/ManualWiringPanel.py index a9ab6a7..55c7634 100644 --- a/src/Mod/FreeCADExchange/ManualWiringPanel.py +++ b/src/Mod/FreeCADExchange/ManualWiringPanel.py @@ -148,11 +148,30 @@ def _selected_point(): def _selected_terminal(): for obj in _selection(): - if TerminalObjects.is_terminal_object(obj): + if TerminalObjects.is_terminal_object(obj) and _is_qet_bound_terminal(obj): return obj return None +def _selected_local_terminal(): + for obj in _selection(): + if TerminalObjects.is_terminal_object(obj) and not _is_qet_bound_terminal(obj): + return obj + return None + + +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 _is_wire_task_object(obj): if obj is None: return False @@ -201,6 +220,8 @@ def _find_terminal_by_uuid(doc, terminal_uuid, element_uuid=""): target_element = (element_uuid or "").strip() fallback = None for terminal in _iter_terminal_objects(doc): + if not _is_qet_bound_terminal(terminal): + continue if getattr(terminal, "QetTerminalUuid", "").strip() != target: continue if not target_element: @@ -863,6 +884,10 @@ class ManualWiringController: def set_start_from_selection(self): terminal = _selected_terminal() if terminal is None: + if _selected_local_terminal() is not None: + raise ManualWiringPanelError( + "所选端子是 FreeCAD 本地端子,请选择 QET 绑定工程端子。" + ) raise ManualWiringPanelError("请先选择一个工程端子,再点击“设为起点”。") self._reset_route_state() self.start_terminal = terminal @@ -966,8 +991,12 @@ class ManualWiringController: wire_kwargs = _task_wire_kwargs(self.current_task) else: end_terminal = _selected_terminal() - if end_terminal is None: - raise ManualWiringPanelError("请先选择一个工程端子,再点击“设为终点并生成”。") + if end_terminal is None: + if _selected_local_terminal() is not None: + raise ManualWiringPanelError( + "所选端子是 FreeCAD 本地端子,请选择 QET 绑定工程端子。" + ) + raise ManualWiringPanelError("请先选择一个工程端子,再点击“设为终点并生成”。") opened = _open_transaction(doc, "生成手动导线") try: diff --git a/src/Mod/FreeCADExchange/TemplateInstantiation.py b/src/Mod/FreeCADExchange/TemplateInstantiation.py index 64a9bfd..1f9a59d 100644 --- a/src/Mod/FreeCADExchange/TemplateInstantiation.py +++ b/src/Mod/FreeCADExchange/TemplateInstantiation.py @@ -58,12 +58,6 @@ def _project_uuid_for_document(doc): 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): @@ -73,6 +67,18 @@ def _existing_terminal_by_slot(terminal_group): 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( @@ -138,6 +144,7 @@ def ensure_engineering_terminals_for_device(doc, device_group): "created_terminals": 0, "updated_terminals": 0, "skipped_slots": 0, + "skipped_unbound_slots": 0, "skipped_devices_without_template_slots": 0, "local_terminals": 0, "warnings": [], @@ -161,17 +168,26 @@ def ensure_engineering_terminals_for_device(doc, device_group): 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), + 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, + ) ) - terminal_group.addObject(terminal_obj) - report["created_terminals"] += 1 + _debug(report["warnings"][-1]) + continue else: try: terminal_obj.Placement = _slot_placement(slot) @@ -180,10 +196,6 @@ def ensure_engineering_terminals_for_device(doc, device_group): 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) - if TerminalObjects.is_local_terminal_uuid(terminal_uuid): - report["local_terminals"] += 1 TerminalObjects.set_terminal_semantics( terminal_obj, @@ -254,6 +266,7 @@ def ensure_engineering_terminals_for_selection_or_all(doc=None): "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 @@ -362,10 +375,11 @@ class CommandCreateEngineeringTerminals: report = ensure_engineering_terminals_for_selection_or_all(App.ActiveDocument) try: App.Console.PrintMessage( - "[FreeCADExchange] 工程端子生成完成:设备 {0} 个,新增 {1} 个,更新 {2} 个,本地端子 {3} 个,跳过无模板设备 {4} 个。\n".format( + "[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"], ) diff --git a/tests/python/freecad_exchange_manual_wiring_panel_test.py b/tests/python/freecad_exchange_manual_wiring_panel_test.py index e3341b3..6fde382 100644 --- a/tests/python/freecad_exchange_manual_wiring_panel_test.py +++ b/tests/python/freecad_exchange_manual_wiring_panel_test.py @@ -198,6 +198,30 @@ def _reload_modules(): class ManualWiringPanelTest(unittest.TestCase): + def test_controller_rejects_local_terminal_as_manual_wiring_start(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + + local_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalLocal") + terminal_objects.set_terminal_semantics( + local_terminal, + "project-1", + "device-a", + "local:instance-a:P1", + "instance-a", + label="P1", + ) + + selection_state["selection"] = [local_terminal] + + with self.assertRaisesRegex(panel.ManualWiringPanelError, "QET 绑定工程端子"): + panel.ManualWiringController().set_start_from_selection() + def test_controller_creates_preview_point_and_records_face_anchor(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() diff --git a/tests/python/freecad_exchange_template_instantiation_test.py b/tests/python/freecad_exchange_template_instantiation_test.py index 0fb6a3f..fa78025 100644 --- a/tests/python/freecad_exchange_template_instantiation_test.py +++ b/tests/python/freecad_exchange_template_instantiation_test.py @@ -123,7 +123,7 @@ def _reload_modules(): class TemplateInstantiationTest(unittest.TestCase): - def test_template_lcs_slots_become_engineering_terminals(self): + def test_template_lcs_slots_without_qet_binding_do_not_create_local_terminals(self): _install_fake_freecad() template_instantiation, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] @@ -154,14 +154,63 @@ class TemplateInstantiationTest(unittest.TestCase): ) terminals = terminal_objects.collect_terminal_objects(terminal_group) - self.assertEqual(1, report["created_terminals"]) + self.assertEqual(0, report["created_terminals"]) + self.assertEqual(1, report["skipped_unbound_slots"]) + self.assertEqual(0, report["local_terminals"]) + self.assertIn("没有 QET 端子绑定", report["warnings"][0]) + self.assertEqual([], terminals) + self.assertTrue(p1.ViewObject.Visibility) + + def test_template_lcs_slots_update_existing_qet_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) + + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="inst-1", + ) + qet_terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_P1") + terminal_group.addObject(qet_terminal) + terminal_objects.set_terminal_semantics( + qet_terminal, + "project-1", + "ct-1", + "terminal-p1", + "inst-1", + label="P1", + slot_name="P1", + ) + + report = template_instantiation.ensure_engineering_terminals_for_device(doc, device) + terminals = terminal_objects.collect_terminal_objects(terminal_group) + + self.assertEqual(0, report["created_terminals"]) + self.assertEqual(1, report["updated_terminals"]) + self.assertEqual(0, report["skipped_unbound_slots"]) 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.assertEqual("local", terminals[0].QetTerminalBindingMode) - self.assertTrue(terminals[0].CanWire) + self.assertEqual("terminal-p1", terminals[0].QetTerminalUuid) + self.assertEqual("qet", terminals[0].QetTerminalBindingMode) self.assertFalse(p1.ViewObject.Visibility) def test_device_without_template_slots_reports_no_created_terminals(self):