fix/以远程版本为主调整回写冲突

dev
Zhaowenlong 2 days ago
parent 076f20b2e8
commit 78075f8935

@ -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:<instance_id>:<slot_name>` 作为本地端子 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 单元测试验证反序端子导入也能准确落到对应模板槽位。
```

@ -7,17 +7,8 @@ from pathlib import Path
import FreeCAD as App
import TerminalObjects as TerminalObjects
try:
import DeviceImport
except Exception:
DeviceImport = None
try:
import WiringObjects
except ImportError:
WiringObjects = None
import TerminalObjects as TerminalObjects
try:
import FreeCADGui as Gui
@ -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:

@ -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__":

@ -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()
Loading…
Cancel
Save