From 78075f8935d04ac4d28859f13da1a98249e0ab18 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 25 May 2026 09:36:49 +0800 Subject: [PATCH] =?UTF-8?q?fix/=E4=BB=A5=E8=BF=9C=E7=A8=8B=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E4=B8=BA=E4=B8=BB=E8=B0=83=E6=95=B4=E5=9B=9E=E5=86=99?= =?UTF-8?q?=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...子显示连线保存回写开发文档.md | 84 +----- src/Mod/FreeCADExchange/ExchangeWriteBack.py | 161 +---------- tests/python/freecad_exchange_wiring_test.py | 6 +- .../python/freecad_exchange_writeback_test.py | 252 ------------------ 4 files changed, 15 insertions(+), 488 deletions(-) delete mode 100644 tests/python/freecad_exchange_writeback_test.py diff --git a/docs/FreeCAD 端子显示连线保存回写开发文档.md b/docs/FreeCAD 端子显示连线保存回写开发文档.md index 01c0514..5423e9f 100644 --- a/docs/FreeCAD 端子显示连线保存回写开发文档.md +++ b/docs/FreeCAD 端子显示连线保存回写开发文档.md @@ -130,11 +130,11 @@ STEP / STP / STE 适合作为模板制作的输入,不建议作为长期带电 > 本地 STEP 只提供几何,不天然提供“哪个位置是端子”。 -所以要实现端子显示和端子连线,本地模板还必须补一层端子语义。当前正式方案采用下面的优先级: +所以要实现端子显示和端子连线,本地模板还必须补一层端子语义。第一版采用下面的优先级: 1. 正式方式:使用 FCStd 模板,在模板里提前放好 LCS 端子对象。 2. 过渡方式:STEP + sidecar JSON,在同目录下保存端子槽位坐标。 -3. 没有模板端子时,不自动创建正式工程端子。 +3. 验证方式:没有模板语义时,临时使用 bbox fallback 生成端子位置。 sidecar 只作为 FreeCAD 端模板辅助文件,不进入第一版数据库绑定主键。 @@ -142,8 +142,6 @@ sidecar 里除了端子坐标,还可以继续补端子朝向,例如 `rotatio FCStd 模板里的 LCS 如果已经带了 Placement 朝向,导入时也要一并保留,这样端子不只是有坐标,还能保留真实出线方向。 -包围盒 fallback 只能作为历史验证思路或调试函数存在,不再作为正式工程端子的生成依据。原因是包围盒猜点无法保证端子落在真实接线位置,容易导致后续手动布线和自动布线都建立在错误坐标上。 - ### 3.1 FCStd 设备模板制作约定 FCStd 设备模板用于解决“这个模型本身就带端子语义”的问题。模板端子是跨工程复用的槽位,不绑定某个具体工程里的 `terminal_uuid`。 @@ -226,64 +224,6 @@ zwl/QET 侧只需要支持 `.FCStd` 的选择、复制、保存和导出路径 详细设计见: - `docs/superpowers/specs/2026-05-20-freecad-fcstd-asset-flow-design.md` -- `docs/FreeCAD 3D模型端子设计方案.md` - -### 3.4 当前端子生成正式规则 - -当前端子方案区分两类对象: - -1. 模板端子:存在于 `.FCStd` 设备模板中,定义设备模型哪里可以接线。 -2. 工程端子:存在于当前项目 `scene.FCStd` 中,真正参与 3D 手动布线和保存回写。 - -模板端子只保存跨工程稳定信息,例如: - -```text -Role = Terminal -CanWire = true -QetTemplateSlotName = P1 -QetTerminalLabel = P1 -QetTerminalType = primary -Placement.Base = 模板局部坐标 -Placement.Rotation = 端子方向 -``` - -工程端子在模板端子基础上生成,并补当前工程绑定信息: - -```text -QetProjectUuid -QetElementUuid -QetTerminalUuid -QetInstanceId -Role = Terminal -CanWire = true -QetTemplateSlotName -``` - -端子方向约定为: - -```text -LCS 本地 +Z 方向 = 出线方向 -``` - -因此模板制作时不只要放准端子位置,还应尽量让 LCS 的 +Z 指向电线离开设备的方向。这个方向会影响后续手动布线的出线段和自动布线的起始方向。 - -自动创建工程端子的前提是: - -1. QET `2d_to_3d.json` 中存在 `terminal_uuid / instance_id`。 -2. FreeCAD 能通过 `instance_id` 找到对应 `QETDevice_xxx`。 -3. 该设备实例导入的 `.FCStd` 中存在模板端子。 - -如果设备没有模板端子,不再自动凭空创建工程端子。此时应先补设备模板端子,或者由用户在 FreeCAD 里手动制作模板端子后保存新的 `.FCStd`。 - -手动生成工程端子的规则是: - -1. 用户选中 `QETDevice_xxx`。 -2. 点击 `QET模板 -> 生成工程端子`。 -3. FreeCAD 扫描该设备下的模板端子。 -4. 按模板端子的局部坐标和方向生成工程端子。 -5. 如果没有 QET 真实 `terminal_uuid`,使用 `local::` 作为本地端子 UUID。 - -`local:*` 端子可以用于 3D 手动布线,但不能当作 QET 2D 端子的可靠回写依据。要准确回写 2D,仍需要 zh/QET 侧导出真实 `terminal_uuid + instance_id` 绑定。 ## 4. 为什么要先落地设备模板 @@ -378,8 +318,7 @@ FreeCADExchange/ TemplateSemantics.py # 新增:读取 FCStd LCS 或 STEP sidecar 端子槽位 TemplateAuthoring.py # 计划新增:把 STEP/STP/STE 制作为带端子语义的 FCStd 模板 TemplateAuthoringPanel.py # 新增:CAD 人员使用的端子制作任务面板 - TerminalImport.py # 新增:根据 terminals 创建/更新真实工程端子对象 - TemplateInstantiation.py # 新增:把模板端子实例化为工程端子,支持本地 local:* 端子 + TerminalImport.py # 新增:根据 terminals 创建/更新端子对象 TerminalObjects.py # 新增:端子对象属性、查找、校验工具 ManualWiring.py # 新增:端子选择、折线路径创建、连线对象属性 ExchangeWriteBack.py # 新增:生成 3d_to_2d.json @@ -422,7 +361,7 @@ ManualWiring.py - `QET_Template_AddTerminal` - `QET_Template_SaveAsFCStd` - 第一版端子位置可以通过用户选择对象/点位后的三维坐标生成。 -- 第一版允许先使用默认方向,但正式设备模板应补齐出线方向;当前约定 LCS 本地 +Z 为出线方向。 +- 第一版端子方向默认使用单位旋转,后续再补出线方向编辑。 模板端子属性: @@ -438,7 +377,7 @@ ManualWiring.py - 添加 `P1`、`P2` 两个端子。 - 保存为 `电流互感器.FCStd`。 - 重新打开该 FCStd 后,端子 LCS 仍存在,属性仍存在。 -- 在项目导入流程中引用该 FCStd,工程端子位置来自模板 LCS,不再依赖 bbox fallback。 +- 在项目导入流程中引用该 FCStd,端子位置优先来自模板 LCS,而不是 bbox fallback。 ### 阶段 A:本地模板导入基线 @@ -478,10 +417,10 @@ ManualWiring.py 端子位置策略: 1. 如果 FCStd 模板已有 `Role="Terminal"` 的 LCS,则优先复用模板 LCS。 -2. 如果有 sidecar JSON,则只作为过渡方案按 sidecar 坐标创建。 -3. 如果没有模板端子,也没有 sidecar,不自动创建正式工程端子。 +2. 如果有 sidecar JSON,则按 sidecar 坐标创建。 +3. 如果只有 STEP,则先按设备包围盒生成临时端子排列,用于打通流程。 -包围盒临时端子只作为历史调试思路,不再作为正式工程端子的创建依据。 +临时端子排列只用于第一版验证,不作为长期物理端子定义。 验收: @@ -603,7 +542,7 @@ ManualWiring.py - 可选 sidecar:只作为过渡或校验,不作为正式交付优先方案 - 模板说明:原点、朝向、尺寸单位、端子数量 -常用设备建议优先补齐 `FCStd LCS`,让工程端子始终来自真实可用的模板端子坐标。 +常用设备建议优先补齐 `FCStd LCS`,把端子位置从临时的 `bbox fallback` 提升为真实可用坐标。 ## 10. 单人开发优先级 @@ -659,9 +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 侧决定是否消费该可选字段。 -- 2026-05-22:完善 3D 手动布线对象模型,新增场景级 `QETWiring` 分组及任务、载体、预览、已布线、诊断分区;手动导线优先使用 Draft Wire,按端子 LCS 本地 +Z 方向生成出线段,同时继续兼容设备下 `QETWires_*` 旧分组;`3d_to_2d.json` 回写现在优先收集 `QETWiring_04_Routed` 中的已布线对象,并避免和旧设备分组重复。已通过 FreeCADExchange Python 单元测试和运行目录 `FreeCADCmd.exe` 模块导入验证。自动布线算法仍由后续独立模块接入。 -- 2026-05-23:补充独立文档 `FreeCAD 3D模型端子设计方案.md`,明确正式路线为 `STEP -> FreeCAD 添加模板端子 -> 保存 FCStd -> 生成工程端子 -> 手动布线`;同步更新本文档,去掉 bbox fallback 作为正式工程端子的说法,明确手动生成工程端子必须以模板端子为准,`local:*` 端子只用于 3D 本地布线,准确回写 QET 仍依赖 `terminal_uuid + instance_id`。 -- 2026-05-23:收口工程端子生成和回写边界,新增 `QetTerminalBindingMode` 标记本地/真实端子,手动生成工程端子时无模板槽位则直接提示并跳过;`3d_to_2d.json` 现在会过滤 `local:*` 端子和本地导线,确保只回写真实 QET 绑定。已通过 FreeCADExchange Python 单元测试全量验证。 -- 2026-05-23:补充 `slot_name_hint / terminal_label` 的端子匹配支持,`TerminalImport` 现在优先按提示名对齐模板槽位,再退回顺序匹配;已用 FreeCADExchange Python 单元测试验证反序端子导入也能准确落到对应模板槽位。 ``` diff --git a/src/Mod/FreeCADExchange/ExchangeWriteBack.py b/src/Mod/FreeCADExchange/ExchangeWriteBack.py index 447aa9d..2e2f28d 100644 --- a/src/Mod/FreeCADExchange/ExchangeWriteBack.py +++ b/src/Mod/FreeCADExchange/ExchangeWriteBack.py @@ -7,18 +7,9 @@ from pathlib import Path import FreeCAD as App +import DeviceImport import TerminalObjects as TerminalObjects -try: - import DeviceImport -except Exception: - DeviceImport = None - -try: - import WiringObjects -except ImportError: - WiringObjects = None - try: import FreeCADGui as Gui except ImportError: @@ -33,8 +24,6 @@ class ExchangeWriteBackError(RuntimeError): def _append_debug_log(message): - if DeviceImport is None: - return try: DeviceImport._append_debug_log(message) except Exception: @@ -60,7 +49,7 @@ def _is_device_group(obj): if obj is None: return False try: - if not obj.Name.startswith(TerminalObjects.DEVICE_GROUP_PREFIX): + if not obj.Name.startswith(DeviceImport.DEVICE_GROUP_PREFIX): return False return "QetElementUuid" in getattr(obj, "PropertiesList", []) except Exception: @@ -90,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: @@ -192,8 +143,6 @@ def _collect_terminal_bindings(doc): for terminal_obj in _iter_terminal_objects(device_group): terminal_uuid = getattr(terminal_obj, "QetTerminalUuid", "").strip() terminal_instance_id = getattr(terminal_obj, "QetInstanceId", "").strip() or instance_id - if TerminalObjects.is_local_terminal_uuid(terminal_uuid): - continue if not terminal_uuid or not terminal_instance_id: continue key = (terminal_uuid, terminal_instance_id) @@ -209,104 +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 _manual_wire_object_key(wire_obj): - return ( - getattr(wire_obj, "QetWireUuid", "").strip(), - getattr(wire_obj, "QetStartTerminalUuid", "").strip(), - getattr(wire_obj, "QetEndTerminalUuid", "").strip(), - getattr(wire_obj, "QetStartInstanceId", "").strip(), - getattr(wire_obj, "QetEndInstanceId", "").strip(), - getattr(wire_obj, "Name", ""), - ) - - -def _legacy_wire_payload(wire_obj): - 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: - return None - if TerminalObjects.is_local_terminal_uuid(start_terminal_uuid) or TerminalObjects.is_local_terminal_uuid(end_terminal_uuid): - return None - - return { - "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), - } - - -def _collect_manual_wires(doc): - wires = [] - seen = set() - seen_objects = set() - - if WiringObjects is not None: - try: - for wire_obj in WiringObjects.iter_routed_wire_objects(doc): - payload = WiringObjects.wire_payload_from_object(wire_obj) - if not payload.get("start_terminal_uuid") or not payload.get("end_terminal_uuid"): - continue - if TerminalObjects.is_local_terminal_uuid(payload.get("start_terminal_uuid")) or TerminalObjects.is_local_terminal_uuid(payload.get("end_terminal_uuid")): - continue - key = _manual_wire_object_key(wire_obj) - seen.add(key) - seen_objects.add(id(wire_obj)) - wires.append(payload) - except Exception as exc: - _append_debug_log("collect scene routed wires failed: {0}".format(exc)) - - for device_group in _iter_device_groups(doc): - for wire_obj in _iter_wire_objects(device_group): - if id(wire_obj) in seen_objects: - continue - - payload = _legacy_wire_payload(wire_obj) - if payload is None: - continue - - key = _manual_wire_object_key(wire_obj) - if key in seen: - continue - seen.add(key) - seen_objects.add(id(wire_obj)) - wires.append(payload) - return wires - - def _project_uuid_from_doc(doc, payload=None): root = _root_group(doc) if root is not None: @@ -341,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, } @@ -355,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, @@ -364,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, ) ) @@ -439,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_wiring_test.py b/tests/python/freecad_exchange_wiring_test.py index 144f40e..351b5a7 100644 --- a/tests/python/freecad_exchange_wiring_test.py +++ b/tests/python/freecad_exchange_wiring_test.py @@ -322,7 +322,7 @@ class WiringTest(unittest.TestCase): self.assertIn("QetManualWaypointsJson", getattr(wire, "PropertiesList", [])) self.assertIn('"support_axis": "x"', getattr(wire, "QetManualWaypointsJson", "")) - def test_wire_writeback_serializes_scene_routed_wire(self): + def test_wire_writeback_omits_scene_routed_wire_payload(self): _install_fake_freecad() terminal_objects, wiring_objects, manual_wiring, write_back = _reload_modules() @@ -345,9 +345,7 @@ class WiringTest(unittest.TestCase): report = write_back.write_back_document(doc, scene_path=r"D:\tmp\scene.FCStd", payload={"project_uuid": "project-1"}) - self.assertEqual(1, len(report["manual_wires"])) - self.assertEqual("terminal-start", report["manual_wires"][0]["start_terminal_uuid"]) - self.assertEqual(2, len(report["manual_wires"][0]["points"])) + self.assertNotIn("manual_wires", report) if __name__ == "__main__": diff --git a/tests/python/freecad_exchange_writeback_test.py b/tests/python/freecad_exchange_writeback_test.py deleted file mode 100644 index bee459f..0000000 --- a/tests/python/freecad_exchange_writeback_test.py +++ /dev/null @@ -1,252 +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_skips_local_terminal_bindings(self): - _install_fake_freecad() - exchange_writeback, terminal_objects = _reload_writeback() - 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") - - terminal_group = terminal_objects.ensure_terminal_group( - doc, - device, - project_uuid="project-1", - instance_id="instance-1", - ) - qet_terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_real") - terminal_group.addObject(qet_terminal) - terminal_objects.set_terminal_semantics( - qet_terminal, - "project-1", - "device-1", - "terminal-real", - "instance-1", - ) - local_terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_local") - terminal_group.addObject(local_terminal) - terminal_objects.set_terminal_semantics( - local_terminal, - "project-1", - "device-1", - "local:instance-1:P1", - "instance-1", - ) - - 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( - [{"terminal_uuid": "terminal-real", "instance_id": "instance-1"}], - report["terminals"], - ) - self.assertEqual(report["terminals"], payload["terminals"]) - - 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], - ) - - def test_write_back_skips_manual_wires_with_local_terminal_endpoints(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_local_terminal") - wire_group.addObject(wire) - terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1") - terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "local:instance-1:P1") - 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([], report["manual_wires"]) - self.assertEqual([], payload["manual_wires"]) - - -if __name__ == "__main__": - unittest.main()