diff --git a/docs/FreeCAD 端子显示连线保存回写开发文档.md b/docs/FreeCAD 端子显示连线保存回写开发文档.md index d8ee2e0..5423e9f 100644 --- a/docs/FreeCAD 端子显示连线保存回写开发文档.md +++ b/docs/FreeCAD 端子显示连线保存回写开发文档.md @@ -598,5 +598,4 @@ ManualWiring.py - 2026-05-20:修复 `TemplateAuthoring.py` 在 `FreeCADCmd.exe` 命令行模式下导入时误注册 GUI 命令的问题;已在运行目录验证创建 `P1` 模板端子、保存 `.FCStd`、重新打开后端子语义仍可识别。 - 2026-05-20:确定方案 2 开发目标:新增“设备模板端子制作”任务面板,让 CAD 工作人员通过输入端子名、选择模型位置、点击按钮完成添加端子、校验端子和保存 FCStd,不再依赖 Python 控制台。 - 2026-05-20:新增 `TemplateAuthoringPanel.py`,提供“设备模板端子制作”任务面板和 `QET_Template_OpenAuthoringPanel` 命令;面板支持输入端子名、添加端子、校验端子、保存 FCStd,并已同步到运行目录验证模块可导入。 -- 2026-05-21:新增手动布线结果回写能力,`3d_to_2d.json` 现在会输出 `manual_wires`,包含起止端子 UUID、起止设备实例 ID、路线类型和 3D 路径点;已用单元测试验证,待 QET 侧决定是否消费该可选字段。 ``` diff --git a/src/Mod/FreeCADExchange/ExchangeWriteBack.py b/src/Mod/FreeCADExchange/ExchangeWriteBack.py index 26bb80b..2e2f28d 100644 --- a/src/Mod/FreeCADExchange/ExchangeWriteBack.py +++ b/src/Mod/FreeCADExchange/ExchangeWriteBack.py @@ -79,44 +79,6 @@ def _iter_terminal_objects(device_group): return TerminalObjects.collect_terminal_objects(terminal_container) -def _iter_wire_objects(device_group): - wire_container = TerminalObjects.find_child_group_by_kind( - device_group, - TerminalObjects.WIRE_GROUP_KIND, - ) - if wire_container is None: - return [] - - result = [] - - def walk(obj): - if obj is None: - return - for child in list(getattr(obj, "Group", []) or []): - if _is_manual_wire_object(child): - result.append(child) - continue - try: - if child.isDerivedFrom("App::DocumentObjectGroup"): - walk(child) - except Exception: - pass - - walk(wire_container) - return result - - -def _is_manual_wire_object(obj): - if obj is None: - return False - properties = getattr(obj, "PropertiesList", []) - if "QetStartTerminalUuid" not in properties: - return False - if "QetEndTerminalUuid" not in properties: - return False - return (getattr(obj, "RouteType", "") or "").strip() == "Manual" - - def _scene_path_from_doc(doc, scene_path=""): candidate = (scene_path or "").strip() if candidate: @@ -196,70 +158,6 @@ def _collect_terminal_bindings(doc): return bindings -def _point_from_vector(vector): - return { - "x": float(getattr(vector, "x", 0.0)), - "y": float(getattr(vector, "y", 0.0)), - "z": float(getattr(vector, "z", 0.0)), - } - - -def _wire_shape_points(wire_obj): - shape = getattr(wire_obj, "Shape", None) - if shape is None: - return [] - - if isinstance(shape, (list, tuple)): - return [_point_from_vector(point) for point in shape] - - vertices = getattr(shape, "Vertexes", None) - if vertices: - points = [] - for vertex in vertices: - point = getattr(vertex, "Point", None) - if point is not None: - points.append(_point_from_vector(point)) - return points - - return [] - - -def _collect_manual_wires(doc): - wires = [] - seen = set() - for device_group in _iter_device_groups(doc): - for wire_obj in _iter_wire_objects(device_group): - start_terminal_uuid = getattr(wire_obj, "QetStartTerminalUuid", "").strip() - end_terminal_uuid = getattr(wire_obj, "QetEndTerminalUuid", "").strip() - start_instance_id = getattr(wire_obj, "QetStartInstanceId", "").strip() - end_instance_id = getattr(wire_obj, "QetEndInstanceId", "").strip() - route_type = (getattr(wire_obj, "RouteType", "") or "").strip() or "Manual" - if not start_terminal_uuid or not end_terminal_uuid: - continue - - key = ( - start_terminal_uuid, - end_terminal_uuid, - start_instance_id, - end_instance_id, - getattr(wire_obj, "Name", ""), - ) - if key in seen: - continue - seen.add(key) - wires.append( - { - "start_terminal_uuid": start_terminal_uuid, - "end_terminal_uuid": end_terminal_uuid, - "start_instance_id": start_instance_id, - "end_instance_id": end_instance_id, - "route_type": route_type, - "points": _wire_shape_points(wire_obj), - } - ) - return wires - - def _project_uuid_from_doc(doc, payload=None): root = _root_group(doc) if root is not None: @@ -294,7 +192,6 @@ def write_back_document(doc=None, scene_path="", payload=None): "generated_at": _format_timestamp(), "instances": _collect_instance_bindings(doc), "terminals": _collect_terminal_bindings(doc), - "manual_wires": _collect_manual_wires(doc), "output_path": output_path, } @@ -308,7 +205,6 @@ def write_back_document(doc=None, scene_path="", payload=None): "generated_at": report["generated_at"], "instances": report["instances"], "terminals": report["terminals"], - "manual_wires": report["manual_wires"], }, ensure_ascii=False, indent=2, @@ -317,10 +213,9 @@ def write_back_document(doc=None, scene_path="", payload=None): ) _append_debug_log( - "write_back_document completed: instances={0}, terminals={1}, manual_wires={2}, path={3}".format( + "write_back_document completed: instances={0}, terminals={1}, path={2}".format( len(report["instances"]), len(report["terminals"]), - len(report["manual_wires"]), output_path, ) ) @@ -392,10 +287,9 @@ class CommandWriteBack: report = write_back_document(App.ActiveDocument) try: App.Console.PrintMessage( - "[FreeCADExchange] Write-back completed: {0} instances, {1} terminals, {2} manual wires\n".format( + "[FreeCADExchange] Write-back completed: {0} instances, {1} terminals\n".format( len(report["instances"]), len(report["terminals"]), - len(report["manual_wires"]), ) ) except Exception: diff --git a/tests/python/freecad_exchange_writeback_test.py b/tests/python/freecad_exchange_writeback_test.py deleted file mode 100644 index 20b0466..0000000 --- a/tests/python/freecad_exchange_writeback_test.py +++ /dev/null @@ -1,158 +0,0 @@ -import importlib -import json -import sys -import tempfile -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) - - fake_freecad = types.ModuleType("FreeCAD") - fake_freecad.Vector = Vector - fake_freecad.ActiveDocument = None - fake_freecad.Console = types.SimpleNamespace( - PrintMessage=lambda *args, **kwargs: None, - PrintWarning=lambda *args, **kwargs: None, - PrintError=lambda *args, **kwargs: None, - ) - fake_freecad.addDocumentObserver = lambda observer: None - sys.modules["FreeCAD"] = fake_freecad - - fake_freecadgui = types.ModuleType("FreeCADGui") - fake_freecadgui.addCommand = lambda *args, **kwargs: None - sys.modules["FreeCADGui"] = fake_freecadgui - - fake_importgui = types.ModuleType("ImportGui") - fake_importgui.insert = lambda *args, **kwargs: None - sys.modules["ImportGui"] = fake_importgui - - fake_device_preview = types.ModuleType("DevicePreview") - fake_device_preview.find_main_exchange_document = lambda preferred_name="": None - sys.modules["DevicePreview"] = fake_device_preview - - -class FakeObject: - def __init__(self, name, type_id="App::DocumentObjectGroup"): - self.Name = name - self.Label = name - self.TypeId = type_id - self.PropertiesList = [] - self.Group = [] - self.InList = [] - - 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"} - 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.FileName = "" - 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 _reload_writeback(): - for name in ["DeviceImport", "TerminalObjects", "ExchangeWriteBack"]: - sys.modules.pop(name, None) - return importlib.import_module("ExchangeWriteBack"), importlib.import_module("TerminalObjects") - - -class ExchangeWriteBackManualWireTest(unittest.TestCase): - def test_write_back_includes_manual_wires_with_route_points(self): - _install_fake_freecad() - exchange_writeback, terminal_objects = _reload_writeback() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - - root = terminal_objects.ensure_root_group(doc, "project-1") - device = doc.addObject("App::Part", "QETDevice_device_1") - root.addObject(device) - terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1") - terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1") - - wire_group = terminal_objects.ensure_wire_group( - doc, - device, - project_uuid="project-1", - instance_id="instance-1", - ) - wire = doc.addObject("Part::Feature", "QETWire_terminal_1_terminal_2") - wire_group.addObject(wire) - terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1") - terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "terminal-1") - terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-2") - terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-1") - terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-2") - terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual") - wire.Shape = ( - app.Vector(1, 2, 3), - app.Vector(4, 5, 6), - ) - - with tempfile.TemporaryDirectory() as temp_dir: - scene_path = str(Path(temp_dir) / "scene.FCStd") - report = exchange_writeback.write_back_document( - doc, - scene_path=scene_path, - payload={"project_uuid": "project-1"}, - ) - payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8")) - - self.assertEqual(1, len(report["manual_wires"])) - self.assertEqual(1, len(payload["manual_wires"])) - self.assertEqual( - { - "start_terminal_uuid": "terminal-1", - "end_terminal_uuid": "terminal-2", - "start_instance_id": "instance-1", - "end_instance_id": "instance-2", - "route_type": "Manual", - "points": [ - {"x": 1.0, "y": 2.0, "z": 3.0}, - {"x": 4.0, "y": 5.0, "z": 6.0}, - ], - }, - payload["manual_wires"][0], - ) - - -if __name__ == "__main__": - unittest.main()