diff --git a/docs/2D-3D交换协议.md b/docs/2D-3D交换协议.md index ff20fbd..dddacae 100644 --- a/docs/2D-3D交换协议.md +++ b/docs/2D-3D交换协议.md @@ -293,13 +293,13 @@ "group_uuid": "string", "wire_mark": "string", "wire_mark_is_manual": false, + "wire_style_id": 0, "start_element_uuid": "string", "start_terminal_uuid": "string", "end_element_uuid": "string", "end_terminal_uuid": "string", "start_terminal_display": "string", - "end_terminal_display": "string", - "conductor_uuids": [] + "end_terminal_display": "string" } ``` @@ -307,18 +307,18 @@ | 字段 | 中文 | 必需 | 说明 | | --- | --- | --- | --- | -| `wire_id` | 导线交换ID | 是 | JSON 交换层稳定标识;优先使用单根导线 UUID,退回 `net_uuid + index` | +| `wire_id` | 导线交换ID | 是 | JSON 交换层稳定标识;按 `DirectionInfo` 生成,格式为 `direction:${net_uuid}:${index}` | | `net_uuid` | 网络UUID | 否 | 当前逻辑导线所属网络 | | `group_uuid` | 网络分组UUID | 否 | 当前逻辑导线所属网络分组 | | `wire_mark` | 导线标注 | 否 | 导线当前标注;为空时导出为 `无标注导线` | | `wire_mark_is_manual` | 导线标注是否手工 | 否 | 是否手工修改过导线标注 | +| `wire_style_id` | 导线样式ID | 否 | 取 `start_terminal` 所连接导线的样式 ID | | `start_element_uuid` | 起点设备UUID | 是 | 起点端子所属 2D 设备实例 | | `start_terminal_uuid` | 起点端子UUID | 是 | 起点 2D 端子实例 | | `end_element_uuid` | 终点设备UUID | 是 | 终点端子所属 2D 设备实例 | | `end_terminal_uuid` | 终点端子UUID | 是 | 终点 2D 端子实例 | | `start_terminal_display` | 起点端子显示号 | 否 | 起点端子在 QET 中的显示编号 | | `end_terminal_display` | 终点端子显示号 | 否 | 终点端子在 QET 中的显示编号 | -| `conductor_uuids` | 几何导线UUID列表 | 否 | 当前逻辑导线对应的 2D 几何导线 UUID 列表 | ### 8.4 说明 @@ -326,6 +326,12 @@ - 它是**导线标注** - 不是设备实例标注,也不是符号设备标注 +- `wire_id` 代表一条 `DirectionInfo` +- 不再混入几何 `Conductor` UUID 作为导线主标识 + +- `wire_style_id` 只取 `start_terminal` 所连接导线的样式 +- 不按整条几何路径聚合多个样式 + - `wires` 是交换 JSON 的扩展层 - 不意味着第一版数据库绑定表要新增导线绑定表 diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index b4ea5f9..053e19a 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -166,6 +166,30 @@ def _ensure_child_group(doc, parent_group, element_uuid, instance_id, name_prefi def _ensure_document(scene_path): preferred_name = _safe_token(Path(scene_path).stem if scene_path else "QETScene")[:48] or "QETScene" + normalized_scene_path = _native_path(scene_path) + if normalized_scene_path and os.path.isfile(normalized_scene_path): + normalized_target = os.path.normcase(os.path.normpath(normalized_scene_path)) + for candidate in App.listDocuments().values(): + candidate_path = getattr(candidate, "FileName", "") or "" + if candidate_path and os.path.normcase(os.path.normpath(candidate_path)) == normalized_target: + _activate_document(candidate) + return candidate + + try: + doc = App.openDocument(normalized_scene_path) + except Exception as exc: + raise DeviceImportError( + "Cannot open existing FreeCAD scene file: {0}".format(normalized_scene_path) + ) from exc + + if doc is None: + raise DeviceImportError( + "Cannot open existing FreeCAD scene file: {0}".format(normalized_scene_path) + ) + + _activate_document(doc) + return doc + existing_doc = DevicePreview.find_main_exchange_document(preferred_name) if existing_doc is not None: _activate_document(existing_doc) diff --git a/src/Mod/FreeCADExchange/ExchangeBootstrap.py b/src/Mod/FreeCADExchange/ExchangeBootstrap.py index 3a6582a..b41ba02 100644 --- a/src/Mod/FreeCADExchange/ExchangeBootstrap.py +++ b/src/Mod/FreeCADExchange/ExchangeBootstrap.py @@ -371,6 +371,7 @@ def _normalize_terminals(payload): "terminal_uuid": terminal_uuid, "instance_id": _normalize_instance_id(item), "element_uuid": element_uuid.strip() if isinstance(element_uuid, str) else "", + "terminal_display": _optional_string(item, "terminal_display", "terminal entry #{0}".format(index)), } ) return normalized diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index 48a5e09..3419203 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -35,8 +35,10 @@ def _normalize_terminal_entry(item, index): instance_id = (item.get("instance_id") or "").strip() element_uuid = (item.get("element_uuid") or "").strip() + terminal_display = (item.get("terminal_display") or "").strip() slot_name_hint = ( item.get("slot_name_hint") + or item.get("terminal_display") or item.get("terminal_label") or item.get("slot_name") or item.get("display_tag") @@ -47,6 +49,7 @@ def _normalize_terminal_entry(item, index): "terminal_uuid": terminal_uuid, "instance_id": instance_id, "element_uuid": element_uuid, + "terminal_display": terminal_display, "slot_name_hint": slot_name_hint, } @@ -168,6 +171,13 @@ def _terminal_slot_label(slot, terminal_uuid): return terminal_uuid +def _terminal_entry_label(entry, slot, terminal_uuid): + entry_label = (entry.get("terminal_display") or "").strip() + if entry_label: + return entry_label + return _terminal_slot_label(slot, terminal_uuid) + + def _normalize_slot_name(value): return (value or "").strip().lower() @@ -237,13 +247,14 @@ def _slot_placement(slot): return App.Placement(base, rotation) -def _create_terminal_object(doc, terminal_uuid, slot, terminal_group, project_uuid, element_uuid, instance_id): +def _create_terminal_object(doc, terminal_uuid, entry, slot, terminal_group, project_uuid, element_uuid, instance_id): + terminal_label = _terminal_entry_label(entry, slot, terminal_uuid) name_hint = "QETTerminal_{0}".format(TerminalObjects.safe_token(terminal_uuid)) terminal_obj = TerminalObjects.create_lcs_object( doc, name_hint, placement=_slot_placement(slot), - label=_terminal_slot_label(slot, terminal_uuid), + label=terminal_label, ) terminal_group.addObject(terminal_obj) TerminalObjects.set_terminal_semantics( @@ -252,7 +263,7 @@ def _create_terminal_object(doc, terminal_uuid, slot, terminal_group, project_uu element_uuid, terminal_uuid, instance_id, - label=_terminal_slot_label(slot, terminal_uuid), + label=terminal_label, slot_name=slot.get("name", ""), ) _ensure_visible(terminal_obj) @@ -401,7 +412,7 @@ def import_terminals_from_payload(payload, scene_path=""): device_element_uuid, terminal_uuid, device_instance_id, - label=_terminal_slot_label(slot, terminal_uuid), + label=_terminal_entry_label(entry, slot, terminal_uuid), slot_name=slot.get("name", ""), ) try: @@ -414,6 +425,7 @@ def import_terminals_from_payload(payload, scene_path=""): terminal_obj = _create_terminal_object( doc, terminal_uuid, + entry, slot, terminal_group, project_uuid, @@ -428,7 +440,7 @@ def import_terminals_from_payload(payload, scene_path=""): device_element_uuid, terminal_uuid, device_instance_id, - label=_terminal_slot_label(slot, terminal_uuid), + label=_terminal_entry_label(entry, slot, terminal_uuid), slot_name=slot.get("name", ""), ) try: diff --git a/tests/python/freecad_exchange_device_import_fcstd_test.py b/tests/python/freecad_exchange_device_import_fcstd_test.py index fd238a0..6f20916 100644 --- a/tests/python/freecad_exchange_device_import_fcstd_test.py +++ b/tests/python/freecad_exchange_device_import_fcstd_test.py @@ -1,5 +1,6 @@ import importlib import sys +import tempfile import types import unittest from pathlib import Path @@ -39,6 +40,9 @@ def _install_fake_freecad(source_doc): ) fake_freecad.ActiveDocument = None fake_freecad.set_active_document_calls = [] + fake_freecad.open_document_calls = [] + fake_freecad.new_document_calls = [] + fake_freecad.documents = {} def set_active_document(name): fake_freecad.set_active_document_calls.append(name) @@ -46,9 +50,24 @@ def _install_fake_freecad(source_doc): def close_document(*args, **kwargs): fake_freecad.ActiveDocument = None + def open_document(*args, **kwargs): + fake_freecad.open_document_calls.append((args, kwargs)) + if source_doc is not None: + fake_freecad.documents[source_doc.Name] = source_doc + fake_freecad.ActiveDocument = source_doc + return source_doc + + def new_document(name): + fake_freecad.new_document_calls.append(name) + doc = FakeDocument(name) + fake_freecad.documents[doc.Name] = doc + fake_freecad.ActiveDocument = doc + return doc + fake_freecad.setActiveDocument = set_active_document - fake_freecad.listDocuments = lambda: {} - fake_freecad.openDocument = lambda *args, **kwargs: source_doc + fake_freecad.listDocuments = lambda: dict(fake_freecad.documents) + fake_freecad.openDocument = open_document + fake_freecad.newDocument = new_document fake_freecad.closeDocument = close_document sys.modules["FreeCAD"] = fake_freecad @@ -230,6 +249,25 @@ def _reload_modules(): class FcstdDeviceImportTest(unittest.TestCase): + def test_ensure_document_opens_existing_scene_file_instead_of_creating_new_document(self): + with tempfile.TemporaryDirectory() as temp_dir: + scene_path = Path(temp_dir) / "QETScene.FCStd" + scene_path.write_text("fake fcstd placeholder", encoding="utf-8") + scene_doc = FakeDocument("QETScene", str(scene_path)) + _install_fake_freecad(scene_doc) + app = sys.modules["FreeCAD"] + + device_import, _ = _reload_modules() + + doc = device_import._ensure_document(str(scene_path)) + + self.assertIs(doc, scene_doc) + self.assertEqual(1, len(app.open_document_calls)) + self.assertEqual(str(scene_path), app.open_document_calls[0][0][0]) + self.assertEqual([], app.new_document_calls) + self.assertIs(app.ActiveDocument, scene_doc) + self.assertIn("QETScene", app.set_active_document_calls) + def test_fcstd_import_preserves_template_slots_without_live_template_lcs(self): source = FakeDocument("Source", r"D:\models\breaker.FCStd") _install_fake_freecad(source)