From f78f30509fb0a3e129f24609535db5868dcac998 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Thu, 18 Jun 2026 09:56:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feature/=E5=AE=8C=E5=96=84=E5=BE=85?= =?UTF-8?q?=E8=A3=85=E9=85=8D=E8=AE=BE=E5=A4=87=E5=92=8C=E5=B8=83=E7=BA=BF?= =?UTF-8?q?-zwl-0618?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/examples/qet_panel_assembly/README.md | 8 +- .../create_qet_panel_assembly.py | 78 +- .../qet_panel_assembly.FCStd | Bin 84277 -> 58710 bytes .../qet_panel_assembly.step | 19472 +++++++--------- .../qet_panel_assembly_report.json | 36 +- .../verify_qet_panel_assembly_contacts.py | 134 + data/examples/qet_split_cabinet/README.md | 36 + .../create_nau03_split_cabinet.py | 262 + .../nau03_test_cabinet_split.FCStd | Bin 0 -> 34273 bytes .../nau03_test_cabinet_split.step | 6558 ++++++ .../nau03_test_cabinet_split_report.json | 25 + .../verify_nau03_split_cabinet.py | 96 + docs/2D-3D交换协议.md | 16 +- docs/FreeCAD 机柜装配操作文档.md | 42 + ...子显示连线保存回写开发文档.md | 8 +- ...6-06-17-freecad-terminal-access-routing.md | 128 + docs/数据库设计.md | 68 +- src/Mod/FreeCADExchange/AutoRouting.py | 1315 +- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 1264 +- src/Mod/FreeCADExchange/CMakeLists.txt | 1 + src/Mod/FreeCADExchange/DeviceImport.py | 393 +- src/Mod/FreeCADExchange/ExchangeBootstrap.py | 53 +- src/Mod/FreeCADExchange/InitGui.py | 24 + .../PendingDeviceAssemblyPanel.py | 309 + src/Mod/FreeCADExchange/RoutingNetwork.py | 842 +- src/Mod/FreeCADExchange/StaleObjectSync.py | 9 +- src/Mod/FreeCADExchange/TerminalImport.py | 68 +- src/Mod/FreeCADExchange/TerminalObjects.py | 50 + src/Mod/FreeCADExchange/WiringImport.py | 60 +- .../freecad_pending_device_scene_smoke.py | 148 + .../freecad_exchange_auto_routing_test.py | 13116 +++++++---- .../freecad_exchange_bootstrap_wiring_test.py | 145 +- ...eecad_exchange_device_import_fcstd_test.py | 421 +- ...nge_terminal_import_template_slots_test.py | 181 +- .../freecad_exchange_wiring_import_test.py | 90 +- tests/python/freecad_exchange_wiring_test.py | 77 +- 36 files changed, 28510 insertions(+), 17023 deletions(-) create mode 100644 data/examples/qet_panel_assembly/verify_qet_panel_assembly_contacts.py create mode 100644 data/examples/qet_split_cabinet/README.md create mode 100644 data/examples/qet_split_cabinet/create_nau03_split_cabinet.py create mode 100644 data/examples/qet_split_cabinet/nau03_test_cabinet_split.FCStd create mode 100644 data/examples/qet_split_cabinet/nau03_test_cabinet_split.step create mode 100644 data/examples/qet_split_cabinet/nau03_test_cabinet_split_report.json create mode 100644 data/examples/qet_split_cabinet/verify_nau03_split_cabinet.py create mode 100644 docs/superpowers/plans/2026-06-17-freecad-terminal-access-routing.md create mode 100644 src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py create mode 100644 tests/manual/freecad_pending_device_scene_smoke.py diff --git a/data/examples/qet_panel_assembly/README.md b/data/examples/qet_panel_assembly/README.md index c80692b..002593c 100644 --- a/data/examples/qet_panel_assembly/README.md +++ b/data/examples/qet_panel_assembly/README.md @@ -8,6 +8,7 @@ This directory contains a reusable electrical panel assembly model based on the - `qet_panel_assembly.step`: geometry-only exchange export. - `qet_panel_assembly_report.json`: generated metadata for verification. - `create_qet_panel_assembly.py`: FreeCAD Python generator used to recreate the asset. +- `verify_qet_panel_assembly_contacts.py`: checks the key face-contact constraints after regeneration. ## Geometry @@ -15,11 +16,11 @@ The model is a medium-detail approximation of the full assembly: - Pale gray cabinet / door body. - Thick side edge and recessed door panel. -- Two circular hinge / screw markers. -- Dark right-side mounting plate. +- Dark rear mounting plate centered on the main body's wide face. - Two vertical white perforated connector banks. - Small lower accessory connector modules. -- Yellow wire-frame style guide geometry matching the highlighted wiring envelope in the reference. + +The left thin side face touches the main rectangular body directly. The rear mounting plate is attached to the main body's wide face and centered on it, and the connector banks / lower accessory connectors sit on that mounting plate. Approximate dimensions: @@ -40,4 +41,5 @@ On this Windows workstation, use the registered FreeCAD runtime: $runtime = Get-Content -LiteralPath 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' -Raw | ConvertFrom-Json $env:QET_FREECAD_RUNTIME_JSON = 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' & $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_panel_assembly\create_qet_panel_assembly.py' +& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_panel_assembly\verify_qet_panel_assembly_contacts.py' ``` diff --git a/data/examples/qet_panel_assembly/create_qet_panel_assembly.py b/data/examples/qet_panel_assembly/create_qet_panel_assembly.py index 9c9e118..ed082b0 100644 --- a/data/examples/qet_panel_assembly/create_qet_panel_assembly.py +++ b/data/examples/qet_panel_assembly/create_qet_panel_assembly.py @@ -84,30 +84,6 @@ def _cylinder_y(doc, name, radius, length, x, y, z, color): return obj -def _wire_segment(doc, name, start, end, thickness, color): - sx, sy, sz = start - ex, ey, ez = end - dx = abs(ex - sx) or thickness - dy = abs(ey - sy) or thickness - dz = abs(ez - sz) or thickness - x = min(sx, ex) - (thickness / 2.0 if ex == sx else 0.0) - y = min(sy, ey) - (thickness / 2.0 if ey == sy else 0.0) - z = min(sz, ez) - (thickness / 2.0 if ez == sz else 0.0) - return _box(doc, name, dx, dy, dz, x, y, z, color, 0) - - -def _wire_rect(doc, prefix, x0, y0, z0, width, height, depth_offset=0.0): - color = (1.0, 0.86, 0.05) - y = y0 + depth_offset - thickness = 0.55 - return [ - _wire_segment(doc, prefix + "_Left", (x0, y, z0), (x0, y, z0 + height), thickness, color), - _wire_segment(doc, prefix + "_Right", (x0 + width, y, z0), (x0 + width, y, z0 + height), thickness, color), - _wire_segment(doc, prefix + "_Top", (x0, y, z0 + height), (x0 + width, y, z0 + height), thickness, color), - _wire_segment(doc, prefix + "_Bottom", (x0, y, z0), (x0 + width, y, z0), thickness, color), - ] - - def _export_step(objects): try: import Import @@ -141,24 +117,11 @@ def _create_connector_bank(doc, prefix, x, y, z, rows, cols, plate_height, plate 1.0, 0.7, px, - y - 0.15, + y + 4.15, pz, dark, ) ) - if col == 0: - objects.append( - _cylinder_y( - doc, - "{0}_Screw_R{1:02d}".format(prefix, row + 1), - 0.55, - 0.8, - x + plate_width + 2.6, - y - 0.15, - pz, - metal, - ) - ) return objects @@ -172,46 +135,37 @@ def main(): dark_panel = (0.22, 0.2, 0.26) white = (0.95, 0.94, 0.96) black = (0.02, 0.02, 0.02) + mount_x = -47.5 + mount_y = 30.0 + component_y = 36.0 # Cabinet / door, roughly matching the large pale box in the reference video. objects.extend( [ _box(doc, "Panel_BackBox", 110.0, 55.0, 180.0, -80.0, -25.0, 0.0, light, 0), - _box(doc, "Panel_LeftDoorFace", 6.0, 60.0, 178.0, -88.0, -28.0, 1.0, edge, 0), + _box(doc, "Panel_LeftDoorFace", 6.0, 60.0, 178.0, -86.0, -28.0, 1.0, edge, 0), _box(doc, "Panel_InnerRecess", 72.0, 1.4, 132.0, -68.0, -29.0, 24.0, (0.9, 0.92, 0.96), 18), _box(doc, "Panel_RecessLeftLine", 1.2, 1.8, 132.0, -68.0, -30.0, 24.0, edge, 0), _box(doc, "Panel_RecessRightLine", 1.2, 1.8, 132.0, 2.8, -30.0, 24.0, edge, 0), _box(doc, "Panel_RecessTopLine", 72.0, 1.8, 1.2, -68.0, -30.0, 155.0, edge, 0), _box(doc, "Panel_RecessBottomLine", 72.0, 1.8, 1.2, -68.0, -30.0, 24.0, edge, 0), - _cylinder_y(doc, "Panel_HingeTop", 2.3, 1.0, -85.0, -31.0, 134.0, (0.18, 0.18, 0.2)), - _cylinder_y(doc, "Panel_HingeBottom", 2.3, 1.0, -85.0, -31.0, 44.0, (0.18, 0.18, 0.2)), - _box(doc, "Panel_RightMountPlate", 45.0, 6.0, 168.0, 30.0, -31.0, 6.0, dark_panel, 0), + _box(doc, "Panel_RightMountPlate", 45.0, 6.0, 168.0, mount_x, mount_y, 6.0, dark_panel, 0), ] ) # Connector banks and small accessory blocks on the right side. - objects.extend(_create_connector_bank(doc, "ConnectorBank_Left", 35.0, -36.0, 44.0, 10, 2, 110.0, 18.0)) - objects.extend(_create_connector_bank(doc, "ConnectorBank_Right", 60.0, -37.0, 50.0, 12, 3, 102.0, 20.0)) - objects.extend( - [ - _box(doc, "ConnectorBank_LeftTopCap", 18.0, 4.2, 12.0, 35.0, -36.2, 154.0, white, 0), - _box(doc, "ConnectorBank_RightTopCap", 20.0, 4.2, 11.0, 60.0, -37.2, 152.0, white, 0), - _box(doc, "AccessoryConnector_LowerLeft", 16.0, 4.0, 34.0, 26.0, -37.0, 18.0, white, 0), - _box(doc, "AccessoryConnector_LowerRight", 14.0, 4.0, 28.0, 70.0, -38.0, 22.0, white, 0), - _cylinder_y(doc, "AccessoryConnector_LowerLeftScrew1", 0.8, 0.7, 34.0, -38.1, 28.0, black), - _cylinder_y(doc, "AccessoryConnector_LowerLeftScrew2", 0.8, 0.7, 34.0, -38.1, 42.0, black), - _cylinder_y(doc, "AccessoryConnector_LowerRightScrew1", 0.8, 0.7, 77.0, -39.1, 30.0, black), - _cylinder_y(doc, "AccessoryConnector_LowerRightScrew2", 0.8, 0.7, 77.0, -39.1, 44.0, black), - ] - ) - - # Yellow annotation-like wire frames from the source video. - objects.extend(_wire_rect(doc, "WireFrame_LeftBank", 32.0, -41.0, 34.0, 27.0, 130.0)) - objects.extend(_wire_rect(doc, "WireFrame_RightBank", 56.0, -42.0, 38.0, 33.0, 126.0, -1.2)) + objects.extend(_create_connector_bank(doc, "ConnectorBank_Left", mount_x + 5.0, component_y, 44.0, 10, 2, 110.0, 18.0)) + objects.extend(_create_connector_bank(doc, "ConnectorBank_Right", mount_x + 30.0, component_y, 50.0, 12, 3, 102.0, 20.0)) objects.extend( [ - _wire_segment(doc, "WireFrame_TopBridge", (45.0, -42.0, 158.0), (88.0, -42.0, 158.0), 0.55, (1.0, 0.86, 0.05)), - _wire_segment(doc, "WireFrame_BottomBridge", (42.0, -42.0, 34.0), (88.0, -42.0, 34.0), 0.55, (1.0, 0.86, 0.05)), + _box(doc, "ConnectorBank_LeftTopCap", 18.0, 4.2, 12.0, mount_x + 5.0, component_y, 154.0, white, 0), + _box(doc, "ConnectorBank_RightTopCap", 20.0, 4.2, 11.0, mount_x + 30.0, component_y, 152.0, white, 0), + _box(doc, "AccessoryConnector_LowerLeft", 16.0, 4.0, 34.0, mount_x - 4.0, component_y, 18.0, white, 0), + _box(doc, "AccessoryConnector_LowerRight", 14.0, 4.0, 28.0, mount_x + 36.0, component_y, 22.0, white, 0), + _cylinder_y(doc, "AccessoryConnector_LowerLeftScrew1", 0.8, 0.7, mount_x + 4.0, component_y + 4.1, 28.0, black), + _cylinder_y(doc, "AccessoryConnector_LowerLeftScrew2", 0.8, 0.7, mount_x + 4.0, component_y + 4.1, 42.0, black), + _cylinder_y(doc, "AccessoryConnector_LowerRightScrew1", 0.8, 0.7, mount_x + 43.0, component_y + 4.1, 30.0, black), + _cylinder_y(doc, "AccessoryConnector_LowerRightScrew2", 0.8, 0.7, mount_x + 43.0, component_y + 4.1, 44.0, black), ] ) diff --git a/data/examples/qet_panel_assembly/qet_panel_assembly.FCStd b/data/examples/qet_panel_assembly/qet_panel_assembly.FCStd index 81d705b9aed14d70161dbfe8f9e21e0dcee9ddbd..78cfdc0d2e8629313468c451ad62312185105a45 100644 GIT binary patch literal 58710 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 TOLERANCE: + raise AssertionError("{0}: expected {1:.3f}, got {2:.3f}".format(label, expected, actual)) + + +def main(): + if not FCSTD_PATH.exists(): + raise AssertionError("missing FCStd: {0}".format(FCSTD_PATH)) + if not STEP_PATH.exists(): + raise AssertionError("missing STEP: {0}".format(STEP_PATH)) + if "ISO-10303-21" not in STEP_PATH.read_text(encoding="utf-8", errors="ignore")[:256]: + raise AssertionError("STEP output is missing ISO-10303-21 header") + + doc = App.openDocument(str(FCSTD_PATH)) + backbox = _bbox(doc, "Panel_BackBox") + left_face = _bbox(doc, "Panel_LeftDoorFace") + mount_plate = _bbox(doc, "Panel_RightMountPlate") + object_names = {obj.Name for obj in doc.Objects} + + removed_objects = sorted( + name + for name in object_names + if name.startswith("WireFrame_") + or "_Screw_R" in name + or name in {"Panel_HingeTop", "Panel_HingeBottom"} + ) + if removed_objects: + raise AssertionError("removed guide/hinge objects are still present: {0}".format(removed_objects)) + + _assert_close("left thin face touches main body", left_face.XMax, backbox.XMin) + _assert_close("right mounting plate touches rear of main body", mount_plate.YMin, backbox.YMax) + _assert_close( + "right mounting plate is centered on main body's wide face", + (mount_plate.XMin + mount_plate.XMax) / 2.0, + (backbox.XMin + backbox.XMax) / 2.0, + ) + if mount_plate.XMin < backbox.XMin - TOLERANCE or mount_plate.XMax > backbox.XMax + TOLERANCE: + raise AssertionError( + "right mounting plate must sit within main body's wide face: " + "plate X=({0:.3f}, {1:.3f}), body X=({2:.3f}, {3:.3f})".format( + mount_plate.XMin, + mount_plate.XMax, + backbox.XMin, + backbox.XMax, + ) + ) + + for name in ( + "ConnectorBank_Left_Body", + "ConnectorBank_Right_Body", + "AccessoryConnector_LowerLeft", + "AccessoryConnector_LowerRight", + ): + bbox = _bbox(doc, name) + _assert_close("{0} touches mounting plate".format(name), bbox.YMin, mount_plate.YMax) + if bbox.XMin < backbox.XMin - TOLERANCE or bbox.XMax > backbox.XMax + TOLERANCE: + raise AssertionError( + "{0} must sit within main body's wide face: object X=({1:.3f}, {2:.3f}), body X=({3:.3f}, {4:.3f})".format( + name, + bbox.XMin, + bbox.XMax, + backbox.XMin, + backbox.XMax, + ) + ) + + print("verified qet_panel_assembly contact constraints") + + +if __name__ == "__main__": + main() diff --git a/data/examples/qet_split_cabinet/README.md b/data/examples/qet_split_cabinet/README.md new file mode 100644 index 0000000..8b52766 --- /dev/null +++ b/data/examples/qet_split_cabinet/README.md @@ -0,0 +1,36 @@ +# NAU03 Split Cabinet Asset + +This directory contains a simplified NAU03-style electrical cabinet STEP asset for QET / FreeCAD assembly tests. + +## Outputs + +- `nau03_test_cabinet_split.step`: STEP model with cabinet faces split into independently hideable parts. +- `nau03_test_cabinet_split.FCStd`: FreeCAD source document generated for inspection and regeneration. +- `nau03_test_cabinet_split_report.json`: Generated metadata. +- `create_nau03_split_cabinet.py`: FreeCAD Python generator. +- `verify_nau03_split_cabinet.py`: Import verification script. + +## Hideable Parts + +The STEP is intentionally exported at medium granularity: + +- `NAU03_Cabinet_Frame` +- `NAU03_Left_Side_Panel` +- `NAU03_Right_Side_Panel` +- `NAU03_Rear_Panel` +- `NAU03_Front_Left_Door` +- `NAU03_Front_Right_Door` +- `NAU03_Top_Roof` +- `NAU03_Bottom_Base` +- `NAU03_Interior_Mounting_Plate` + +This avoids the original behavior where hiding the imported cabinet hides a large assembly at once, while also avoiding hundreds of tiny screw and hinge tree objects. + +## Regenerate + +```powershell +$runtime = Get-Content -LiteralPath 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' -Raw | ConvertFrom-Json +$env:QET_FREECAD_RUNTIME_JSON = 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' +& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_split_cabinet\create_nau03_split_cabinet.py' +& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_split_cabinet\verify_nau03_split_cabinet.py' +``` diff --git a/data/examples/qet_split_cabinet/create_nau03_split_cabinet.py b/data/examples/qet_split_cabinet/create_nau03_split_cabinet.py new file mode 100644 index 0000000..05bbc1a --- /dev/null +++ b/data/examples/qet_split_cabinet/create_nau03_split_cabinet.py @@ -0,0 +1,262 @@ +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + + +def _bootstrap_windows_freecad_runtime() -> None: + if os.name != "nt": + return + + runtime_json = os.environ.get("QET_FREECAD_RUNTIME_JSON") + if not runtime_json: + runtime_json = os.path.join(os.environ.get("LOCALAPPDATA", ""), "QETDeps", "runtime.json") + if not runtime_json or not os.path.exists(runtime_json): + return + + with open(runtime_json, "r", encoding="utf-8-sig") as handle: + runtime = json.load(handle) + + roots = [str(item) for item in runtime.get("path_prefix", []) if item] + freecad_root = runtime.get("freecad_root", "") + roots.extend( + [ + os.path.join(freecad_root, "build", "Mod", "Material"), + os.path.join(freecad_root, "build", "Mod", "Part"), + os.path.join(freecad_root, "build", "Mod", "Import"), + os.path.join(freecad_root, "build", "Mod"), + os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "downlevel"), + ] + ) + + for root in roots: + if root and os.path.isdir(root): + try: + os.add_dll_directory(root) + except (AttributeError, OSError): + pass + if root not in sys.path: + sys.path.append(root) + + +_bootstrap_windows_freecad_runtime() + +import FreeCAD as App +import Part + + +OUT_DIR = Path(__file__).resolve().parent +FCSTD_PATH = OUT_DIR / "nau03_test_cabinet_split.FCStd" +STEP_PATH = OUT_DIR / "nau03_test_cabinet_split.step" +REPORT_PATH = OUT_DIR / "nau03_test_cabinet_split_report.json" + + +def _style(obj, color, transparency=0) -> None: + if not hasattr(obj, "ViewObject") or obj.ViewObject is None: + return + obj.ViewObject.ShapeColor = color + obj.ViewObject.Transparency = transparency + + +def _box(dx, dy, dz, x, y, z): + return Part.makeBox(dx, dy, dz, App.Vector(x, y, z)) + + +def _part(doc, name, shape, color, transparency=0): + obj = doc.addObject("Part::Feature", name) + obj.Label = name + obj.Shape = shape + _style(obj, color, transparency) + return obj + + +def _fuse(shapes): + valid_shapes = [shape for shape in shapes if shape and not shape.isNull()] + if not valid_shapes: + raise ValueError("expected at least one shape to fuse") + result = valid_shapes[0] + for shape in valid_shapes[1:]: + result = result.fuse(shape) + try: + result = result.removeSplitter() + except Exception: + pass + return result + + +def _windowed_door(x, y, z, width, thickness, height, window_x, window_z, window_w, window_h): + panel = _box(width, thickness, height, x, y, z) + void = _box(window_w, thickness + 6.0, window_h, x + window_x, y - 3.0, z + window_z) + panel = panel.cut(void) + + # Small raised border around the viewing window, matching the reference cabinet silhouette. + border = 12.0 + border_y = y - 4.0 + border_t = 8.0 + border_shapes = [ + _box(window_w + border * 2.0, border_t, border, x + window_x - border, border_y, z + window_z - border), + _box(window_w + border * 2.0, border_t, border, x + window_x - border, border_y, z + window_z + window_h), + _box(border, border_t, window_h, x + window_x - border, border_y, z + window_z), + _box(border, border_t, window_h, x + window_x + window_w, border_y, z + window_z), + ] + handle_w = 16.0 + handle_h = 120.0 + handle = _box(handle_w, 14.0, handle_h, x + width - 42.0, y - 12.0, z + height * 0.48) + return _fuse([panel, handle] + border_shapes) + + +def _frame_shape(width, depth, height): + post = 45.0 + rail = 55.0 + x0 = -width / 2.0 + y0 = -depth / 2.0 + shapes = [] + + for x in (x0, width / 2.0 - post): + for y in (y0, depth / 2.0 - post): + shapes.append(_box(post, post, height, x, y, 0.0)) + + for z in (0.0, height - rail): + shapes.extend( + [ + _box(width, rail, rail, x0, y0, z), + _box(width, rail, rail, x0, depth / 2.0 - rail, z), + _box(rail, depth, rail, x0, y0, z), + _box(rail, depth, rail, width / 2.0 - rail, y0, z), + ] + ) + return _fuse(shapes) + + +def _vent_slots(x, y, z, width, thickness, height, count): + panel = _box(width, thickness, height, x, y, z) + slot_w = width * 0.62 + slot_h = 16.0 + pitch = 42.0 + start_z = z + height * 0.22 + for index in range(count): + sx = x + (width - slot_w) / 2.0 + sz = start_z + index * pitch + panel = panel.cut(_box(slot_w, thickness + 6.0, slot_h, sx, y - 3.0, sz)) + return panel + + +def _export_step(objects) -> None: + try: + import Import + + Import.export(objects, str(STEP_PATH)) + except Exception: + import ImportGui + + ImportGui.export(objects, str(STEP_PATH)) + + +def main() -> None: + OUT_DIR.mkdir(parents=True, exist_ok=True) + doc = App.newDocument("NAU03_Test_Cabinet_Split") + + width = 750.0 + depth = 1300.0 + height = 2300.0 + side_t = 32.0 + door_t = 36.0 + roof_h = 75.0 + base_h = 110.0 + + metal = (0.62, 0.68, 0.69) + dark = (0.08, 0.09, 0.10) + inner = (0.34, 0.38, 0.40) + + x_min = -width / 2.0 + x_max = width / 2.0 + y_front = -depth / 2.0 + y_back = depth / 2.0 + + door_gap = 8.0 + door_width = (width - 30.0 - door_gap) / 2.0 + door_z = 55.0 + door_h = height - 105.0 + left_door = _windowed_door( + x_min + 12.0, + y_front - door_t, + door_z, + door_width, + door_t, + door_h, + 95.0, + 1260.0, + 185.0, + 470.0, + ) + right_door = _windowed_door( + x_min + 12.0 + door_width + door_gap, + y_front - door_t, + door_z, + door_width, + door_t, + door_h, + 70.0, + 1260.0, + 185.0, + 470.0, + ) + + rear_panel = _box(width - 36.0, side_t, height - 90.0, x_min + 18.0, y_back, 45.0) + left_panel = _vent_slots(x_min - side_t, y_front + 42.0, 50.0, side_t, depth - 84.0, height - 100.0, 10) + right_panel = _vent_slots(x_max, y_front + 42.0, 50.0, side_t, depth - 84.0, height - 100.0, 10) + + roof = _fuse( + [ + _box(width + 70.0, depth + 100.0, roof_h, x_min - 35.0, y_front - 50.0, height), + _box(width + 30.0, 58.0, 45.0, x_min - 15.0, y_front - 10.0, height - 45.0), + _box(width + 30.0, 58.0, 45.0, x_min - 15.0, y_back - 48.0, height - 45.0), + ] + ) + base = _fuse( + [ + _box(width + 35.0, depth + 30.0, base_h, x_min - 17.5, y_front - 15.0, -base_h), + _box(width + 85.0, 95.0, 58.0, x_min - 42.5, y_front - 40.0, -base_h - 58.0), + _box(width + 85.0, 95.0, 58.0, x_min - 42.5, y_back - 55.0, -base_h - 58.0), + ] + ) + mounting_plate = _box(width - 165.0, 14.0, height - 360.0, x_min + 82.5, y_back - 82.0, 180.0) + + objects = [ + _part(doc, "NAU03_Cabinet_Frame", _frame_shape(width, depth, height), dark, 0), + _part(doc, "NAU03_Left_Side_Panel", left_panel, metal, 0), + _part(doc, "NAU03_Right_Side_Panel", right_panel, metal, 0), + _part(doc, "NAU03_Rear_Panel", rear_panel, metal, 0), + _part(doc, "NAU03_Front_Left_Door", left_door, metal, 0), + _part(doc, "NAU03_Front_Right_Door", right_door, metal, 0), + _part(doc, "NAU03_Top_Roof", roof, metal, 0), + _part(doc, "NAU03_Bottom_Base", base, dark, 0), + _part(doc, "NAU03_Interior_Mounting_Plate", mounting_plate, inner, 8), + ] + + doc.recompute() + doc.saveAs(str(FCSTD_PATH)) + _export_step(objects) + + report = { + "source_reference": r"D:\downloadWX\xwechat_files\wxid_pv577xuccot722_5d4a\msg\file\2026-04\MCCB CABINET ASS'Y.STEP", + "outputs": {"fcstd": str(FCSTD_PATH), "step": str(STEP_PATH)}, + "dimensions_mm": { + "width": width, + "depth": depth, + "height_without_roof_base": height, + "overall_height": height + roof_h + base_h + 58.0, + }, + "hideable_parts": [obj.Label for obj in objects], + "object_count": len(objects), + } + REPORT_PATH.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") + print("Generated FCStd: {0}".format(FCSTD_PATH)) + print("Generated STEP: {0}".format(STEP_PATH)) + print("Generated report: {0}".format(REPORT_PATH)) + + +if __name__ == "__main__": + main() diff --git a/data/examples/qet_split_cabinet/nau03_test_cabinet_split.FCStd b/data/examples/qet_split_cabinet/nau03_test_cabinet_split.FCStd new file mode 100644 index 0000000000000000000000000000000000000000..2f19e0aabf295ef25131c69dbe1f3d48264600f2 GIT binary patch literal 34273 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

Z%t3mmk6fK`zhw2!jMu@bvXtjrjoEZjeu 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 literal 0 HcmV?d00001 diff --git a/data/examples/qet_split_cabinet/nau03_test_cabinet_split.step b/data/examples/qet_split_cabinet/nau03_test_cabinet_split.step new file mode 100644 index 0000000..dd3c0f6 --- /dev/null +++ b/data/examples/qet_split_cabinet/nau03_test_cabinet_split.step @@ -0,0 +1,6558 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-06-10T18:05:29',(''),(''), + 'Open CASCADE STEP processor 7.8','FreeCAD','Unknown'); +FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', + 'automotive_design',2000,#2); +#2 = APPLICATION_CONTEXT( + 'core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('NAU03_Test_Cabinet_Split','NAU03_Test_Cabinet_Split','',( + #8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = SHAPE_REPRESENTATION('',(#11,#15,#19,#23,#27,#31,#35,#39,#43,#47), + #51); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18); +#16 = CARTESIAN_POINT('',(0.,0.,0.)); +#17 = DIRECTION('',(0.,0.,1.)); +#18 = DIRECTION('',(1.,0.,0.)); +#19 = AXIS2_PLACEMENT_3D('',#20,#21,#22); +#20 = CARTESIAN_POINT('',(0.,0.,0.)); +#21 = DIRECTION('',(0.,0.,1.)); +#22 = DIRECTION('',(1.,0.,0.)); +#23 = AXIS2_PLACEMENT_3D('',#24,#25,#26); +#24 = CARTESIAN_POINT('',(0.,0.,0.)); +#25 = DIRECTION('',(0.,0.,1.)); +#26 = DIRECTION('',(1.,0.,0.)); +#27 = AXIS2_PLACEMENT_3D('',#28,#29,#30); +#28 = CARTESIAN_POINT('',(0.,0.,0.)); +#29 = DIRECTION('',(0.,0.,1.)); +#30 = DIRECTION('',(1.,0.,0.)); +#31 = AXIS2_PLACEMENT_3D('',#32,#33,#34); +#32 = CARTESIAN_POINT('',(0.,0.,0.)); +#33 = DIRECTION('',(0.,0.,1.)); +#34 = DIRECTION('',(1.,0.,0.)); +#35 = AXIS2_PLACEMENT_3D('',#36,#37,#38); +#36 = CARTESIAN_POINT('',(0.,0.,0.)); +#37 = DIRECTION('',(0.,0.,1.)); +#38 = DIRECTION('',(1.,0.,0.)); +#39 = AXIS2_PLACEMENT_3D('',#40,#41,#42); +#40 = CARTESIAN_POINT('',(0.,0.,0.)); +#41 = DIRECTION('',(0.,0.,1.)); +#42 = DIRECTION('',(1.,0.,0.)); +#43 = AXIS2_PLACEMENT_3D('',#44,#45,#46); +#44 = CARTESIAN_POINT('',(0.,0.,0.)); +#45 = DIRECTION('',(0.,0.,1.)); +#46 = DIRECTION('',(1.,0.,0.)); +#47 = AXIS2_PLACEMENT_3D('',#48,#49,#50); +#48 = CARTESIAN_POINT('',(0.,0.,0.)); +#49 = DIRECTION('',(0.,0.,1.)); +#50 = DIRECTION('',(1.,0.,0.)); +#51 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#55)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#52,#53,#54)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#52 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#53 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#54 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#55 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#52, + 'distance_accuracy_value','confusion accuracy'); +#56 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#57 = SHAPE_DEFINITION_REPRESENTATION(#58,#64); +#58 = PRODUCT_DEFINITION_SHAPE('','',#59); +#59 = PRODUCT_DEFINITION('design','',#60,#63); +#60 = PRODUCT_DEFINITION_FORMATION('','',#61); +#61 = PRODUCT('NAU03_Cabinet_Frame','NAU03_Cabinet_Frame','',(#62)); +#62 = PRODUCT_CONTEXT('',#2,'mechanical'); +#63 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#64 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#65),#875); +#65 = MANIFOLD_SOLID_BREP('',#66); +#66 = CLOSED_SHELL('',(#67,#107,#138,#203,#243,#265,#321,#352,#383,#441, + #472,#496,#520,#537,#589,#620,#644,#708,#739,#763,#787,#804,#834, + #857)); +#67 = ADVANCED_FACE('',(#68),#102,.F.); +#68 = FACE_BOUND('',#69,.F.); +#69 = EDGE_LOOP('',(#70,#80,#88,#96)); +#70 = ORIENTED_EDGE('',*,*,#71,.F.); +#71 = EDGE_CURVE('',#72,#74,#76,.T.); +#72 = VERTEX_POINT('',#73); +#73 = CARTESIAN_POINT('',(-375.,605.,55.)); +#74 = VERTEX_POINT('',#75); +#75 = CARTESIAN_POINT('',(-330.,605.,55.)); +#76 = LINE('',#77,#78); +#77 = CARTESIAN_POINT('',(-375.,605.,55.)); +#78 = VECTOR('',#79,1.); +#79 = DIRECTION('',(1.,0.,-0.)); +#80 = ORIENTED_EDGE('',*,*,#81,.T.); +#81 = EDGE_CURVE('',#72,#82,#84,.T.); +#82 = VERTEX_POINT('',#83); +#83 = CARTESIAN_POINT('',(-375.,605.,2.245E+03)); +#84 = LINE('',#85,#86); +#85 = CARTESIAN_POINT('',(-375.,605.,0.)); +#86 = VECTOR('',#87,1.); +#87 = DIRECTION('',(0.,0.,1.)); +#88 = ORIENTED_EDGE('',*,*,#89,.T.); +#89 = EDGE_CURVE('',#82,#90,#92,.T.); +#90 = VERTEX_POINT('',#91); +#91 = CARTESIAN_POINT('',(-330.,605.,2.245E+03)); +#92 = LINE('',#93,#94); +#93 = CARTESIAN_POINT('',(-375.,605.,2.245E+03)); +#94 = VECTOR('',#95,1.); +#95 = DIRECTION('',(1.,0.,-0.)); +#96 = ORIENTED_EDGE('',*,*,#97,.F.); +#97 = EDGE_CURVE('',#74,#90,#98,.T.); +#98 = LINE('',#99,#100); +#99 = CARTESIAN_POINT('',(-330.,605.,0.)); +#100 = VECTOR('',#101,1.); +#101 = DIRECTION('',(0.,0.,1.)); +#102 = PLANE('',#103); +#103 = AXIS2_PLACEMENT_3D('',#104,#105,#106); +#104 = CARTESIAN_POINT('',(-375.,605.,0.)); +#105 = DIRECTION('',(-0.,1.,0.)); +#106 = DIRECTION('',(0.,0.,1.)); +#107 = ADVANCED_FACE('',(#108),#133,.T.); +#108 = FACE_BOUND('',#109,.T.); +#109 = EDGE_LOOP('',(#110,#111,#119,#127)); +#110 = ORIENTED_EDGE('',*,*,#97,.F.); +#111 = ORIENTED_EDGE('',*,*,#112,.F.); +#112 = EDGE_CURVE('',#113,#74,#115,.T.); +#113 = VERTEX_POINT('',#114); +#114 = CARTESIAN_POINT('',(-330.,650.,55.)); +#115 = LINE('',#116,#117); +#116 = CARTESIAN_POINT('',(-330.,600.,55.)); +#117 = VECTOR('',#118,1.); +#118 = DIRECTION('',(0.,-1.,0.)); +#119 = ORIENTED_EDGE('',*,*,#120,.T.); +#120 = EDGE_CURVE('',#113,#121,#123,.T.); +#121 = VERTEX_POINT('',#122); +#122 = CARTESIAN_POINT('',(-330.,650.,2.245E+03)); +#123 = LINE('',#124,#125); +#124 = CARTESIAN_POINT('',(-330.,650.,0.)); +#125 = VECTOR('',#126,1.); +#126 = DIRECTION('',(0.,0.,1.)); +#127 = ORIENTED_EDGE('',*,*,#128,.T.); +#128 = EDGE_CURVE('',#121,#90,#129,.T.); +#129 = LINE('',#130,#131); +#130 = CARTESIAN_POINT('',(-330.,600.,2.245E+03)); +#131 = VECTOR('',#132,1.); +#132 = DIRECTION('',(0.,-1.,0.)); +#133 = PLANE('',#134); +#134 = AXIS2_PLACEMENT_3D('',#135,#136,#137); +#135 = CARTESIAN_POINT('',(-330.,605.,0.)); +#136 = DIRECTION('',(1.,0.,-0.)); +#137 = DIRECTION('',(0.,0.,1.)); +#138 = ADVANCED_FACE('',(#139,#173),#198,.T.); +#139 = FACE_BOUND('',#140,.T.); +#140 = EDGE_LOOP('',(#141,#151,#159,#167)); +#141 = ORIENTED_EDGE('',*,*,#142,.F.); +#142 = EDGE_CURVE('',#143,#145,#147,.T.); +#143 = VERTEX_POINT('',#144); +#144 = CARTESIAN_POINT('',(-375.,-650.,0.)); +#145 = VERTEX_POINT('',#146); +#146 = CARTESIAN_POINT('',(-375.,650.,0.)); +#147 = LINE('',#148,#149); +#148 = CARTESIAN_POINT('',(-375.,605.,0.)); +#149 = VECTOR('',#150,1.); +#150 = DIRECTION('',(-0.,1.,0.)); +#151 = ORIENTED_EDGE('',*,*,#152,.T.); +#152 = EDGE_CURVE('',#143,#153,#155,.T.); +#153 = VERTEX_POINT('',#154); +#154 = CARTESIAN_POINT('',(-375.,-650.,2.3E+03)); +#155 = LINE('',#156,#157); +#156 = CARTESIAN_POINT('',(-375.,-650.,0.)); +#157 = VECTOR('',#158,1.); +#158 = DIRECTION('',(0.,0.,1.)); +#159 = ORIENTED_EDGE('',*,*,#160,.T.); +#160 = EDGE_CURVE('',#153,#161,#163,.T.); +#161 = VERTEX_POINT('',#162); +#162 = CARTESIAN_POINT('',(-375.,650.,2.3E+03)); +#163 = LINE('',#164,#165); +#164 = CARTESIAN_POINT('',(-375.,-650.,2.3E+03)); +#165 = VECTOR('',#166,1.); +#166 = DIRECTION('',(-0.,1.,0.)); +#167 = ORIENTED_EDGE('',*,*,#168,.F.); +#168 = EDGE_CURVE('',#145,#161,#169,.T.); +#169 = LINE('',#170,#171); +#170 = CARTESIAN_POINT('',(-375.,650.,0.)); +#171 = VECTOR('',#172,1.); +#172 = DIRECTION('',(0.,0.,1.)); +#173 = FACE_BOUND('',#174,.T.); +#174 = EDGE_LOOP('',(#175,#176,#184,#192)); +#175 = ORIENTED_EDGE('',*,*,#81,.T.); +#176 = ORIENTED_EDGE('',*,*,#177,.F.); +#177 = EDGE_CURVE('',#178,#82,#180,.T.); +#178 = VERTEX_POINT('',#179); +#179 = CARTESIAN_POINT('',(-375.,-605.,2.245E+03)); +#180 = LINE('',#181,#182); +#181 = CARTESIAN_POINT('',(-375.,595.,2.245E+03)); +#182 = VECTOR('',#183,1.); +#183 = DIRECTION('',(-0.,1.,0.)); +#184 = ORIENTED_EDGE('',*,*,#185,.F.); +#185 = EDGE_CURVE('',#186,#178,#188,.T.); +#186 = VERTEX_POINT('',#187); +#187 = CARTESIAN_POINT('',(-375.,-605.,55.)); +#188 = LINE('',#189,#190); +#189 = CARTESIAN_POINT('',(-375.,-605.,0.)); +#190 = VECTOR('',#191,1.); +#191 = DIRECTION('',(0.,0.,1.)); +#192 = ORIENTED_EDGE('',*,*,#193,.T.); +#193 = EDGE_CURVE('',#186,#72,#194,.T.); +#194 = LINE('',#195,#196); +#195 = CARTESIAN_POINT('',(-375.,-650.,55.)); +#196 = VECTOR('',#197,1.); +#197 = DIRECTION('',(-0.,1.,0.)); +#198 = PLANE('',#199); +#199 = AXIS2_PLACEMENT_3D('',#200,#201,#202); +#200 = CARTESIAN_POINT('',(-375.,0.,1.15E+03)); +#201 = DIRECTION('',(-1.,-0.,-0.)); +#202 = DIRECTION('',(0.,0.,-1.)); +#203 = ADVANCED_FACE('',(#204),#238,.T.); +#204 = FACE_BOUND('',#205,.T.); +#205 = EDGE_LOOP('',(#206,#216,#224,#232)); +#206 = ORIENTED_EDGE('',*,*,#207,.F.); +#207 = EDGE_CURVE('',#208,#210,#212,.T.); +#208 = VERTEX_POINT('',#209); +#209 = CARTESIAN_POINT('',(-330.,-650.,55.)); +#210 = VERTEX_POINT('',#211); +#211 = CARTESIAN_POINT('',(-330.,-650.,2.245E+03)); +#212 = LINE('',#213,#214); +#213 = CARTESIAN_POINT('',(-330.,-650.,0.)); +#214 = VECTOR('',#215,1.); +#215 = DIRECTION('',(0.,0.,1.)); +#216 = ORIENTED_EDGE('',*,*,#217,.F.); +#217 = EDGE_CURVE('',#218,#208,#220,.T.); +#218 = VERTEX_POINT('',#219); +#219 = CARTESIAN_POINT('',(-330.,-605.,55.)); +#220 = LINE('',#221,#222); +#221 = CARTESIAN_POINT('',(-330.,-650.,55.)); +#222 = VECTOR('',#223,1.); +#223 = DIRECTION('',(0.,-1.,0.)); +#224 = ORIENTED_EDGE('',*,*,#225,.T.); +#225 = EDGE_CURVE('',#218,#226,#228,.T.); +#226 = VERTEX_POINT('',#227); +#227 = CARTESIAN_POINT('',(-330.,-605.,2.245E+03)); +#228 = LINE('',#229,#230); +#229 = CARTESIAN_POINT('',(-330.,-605.,0.)); +#230 = VECTOR('',#231,1.); +#231 = DIRECTION('',(0.,0.,1.)); +#232 = ORIENTED_EDGE('',*,*,#233,.T.); +#233 = EDGE_CURVE('',#226,#210,#234,.T.); +#234 = LINE('',#235,#236); +#235 = CARTESIAN_POINT('',(-330.,-650.,2.245E+03)); +#236 = VECTOR('',#237,1.); +#237 = DIRECTION('',(0.,-1.,0.)); +#238 = PLANE('',#239); +#239 = AXIS2_PLACEMENT_3D('',#240,#241,#242); +#240 = CARTESIAN_POINT('',(-330.,-650.,0.)); +#241 = DIRECTION('',(1.,0.,-0.)); +#242 = DIRECTION('',(0.,0.,1.)); +#243 = ADVANCED_FACE('',(#244),#260,.T.); +#244 = FACE_BOUND('',#245,.T.); +#245 = EDGE_LOOP('',(#246,#252,#253,#259)); +#246 = ORIENTED_EDGE('',*,*,#247,.F.); +#247 = EDGE_CURVE('',#186,#218,#248,.T.); +#248 = LINE('',#249,#250); +#249 = CARTESIAN_POINT('',(-375.,-605.,55.)); +#250 = VECTOR('',#251,1.); +#251 = DIRECTION('',(1.,0.,-0.)); +#252 = ORIENTED_EDGE('',*,*,#185,.T.); +#253 = ORIENTED_EDGE('',*,*,#254,.T.); +#254 = EDGE_CURVE('',#178,#226,#255,.T.); +#255 = LINE('',#256,#257); +#256 = CARTESIAN_POINT('',(-375.,-605.,2.245E+03)); +#257 = VECTOR('',#258,1.); +#258 = DIRECTION('',(1.,0.,-0.)); +#259 = ORIENTED_EDGE('',*,*,#225,.F.); +#260 = PLANE('',#261); +#261 = AXIS2_PLACEMENT_3D('',#262,#263,#264); +#262 = CARTESIAN_POINT('',(-375.,-605.,0.)); +#263 = DIRECTION('',(-0.,1.,0.)); +#264 = DIRECTION('',(0.,0.,1.)); +#265 = ADVANCED_FACE('',(#266,#291),#316,.T.); +#266 = FACE_BOUND('',#267,.T.); +#267 = EDGE_LOOP('',(#268,#269,#277,#285)); +#268 = ORIENTED_EDGE('',*,*,#168,.T.); +#269 = ORIENTED_EDGE('',*,*,#270,.T.); +#270 = EDGE_CURVE('',#161,#271,#273,.T.); +#271 = VERTEX_POINT('',#272); +#272 = CARTESIAN_POINT('',(375.,650.,2.3E+03)); +#273 = LINE('',#274,#275); +#274 = CARTESIAN_POINT('',(-375.,650.,2.3E+03)); +#275 = VECTOR('',#276,1.); +#276 = DIRECTION('',(1.,0.,-0.)); +#277 = ORIENTED_EDGE('',*,*,#278,.F.); +#278 = EDGE_CURVE('',#279,#271,#281,.T.); +#279 = VERTEX_POINT('',#280); +#280 = CARTESIAN_POINT('',(375.,650.,0.)); +#281 = LINE('',#282,#283); +#282 = CARTESIAN_POINT('',(375.,650.,0.)); +#283 = VECTOR('',#284,1.); +#284 = DIRECTION('',(0.,0.,1.)); +#285 = ORIENTED_EDGE('',*,*,#286,.F.); +#286 = EDGE_CURVE('',#145,#279,#287,.T.); +#287 = LINE('',#288,#289); +#288 = CARTESIAN_POINT('',(330.,650.,0.)); +#289 = VECTOR('',#290,1.); +#290 = DIRECTION('',(1.,0.,-0.)); +#291 = FACE_BOUND('',#292,.T.); +#292 = EDGE_LOOP('',(#293,#301,#309,#315)); +#293 = ORIENTED_EDGE('',*,*,#294,.T.); +#294 = EDGE_CURVE('',#113,#295,#297,.T.); +#295 = VERTEX_POINT('',#296); +#296 = CARTESIAN_POINT('',(330.,650.,55.)); +#297 = LINE('',#298,#299); +#298 = CARTESIAN_POINT('',(-375.,650.,55.)); +#299 = VECTOR('',#300,1.); +#300 = DIRECTION('',(1.,0.,-0.)); +#301 = ORIENTED_EDGE('',*,*,#302,.T.); +#302 = EDGE_CURVE('',#295,#303,#305,.T.); +#303 = VERTEX_POINT('',#304); +#304 = CARTESIAN_POINT('',(330.,650.,2.245E+03)); +#305 = LINE('',#306,#307); +#306 = CARTESIAN_POINT('',(330.,650.,0.)); +#307 = VECTOR('',#308,1.); +#308 = DIRECTION('',(0.,0.,1.)); +#309 = ORIENTED_EDGE('',*,*,#310,.F.); +#310 = EDGE_CURVE('',#121,#303,#311,.T.); +#311 = LINE('',#312,#313); +#312 = CARTESIAN_POINT('',(-375.,650.,2.245E+03)); +#313 = VECTOR('',#314,1.); +#314 = DIRECTION('',(1.,0.,-0.)); +#315 = ORIENTED_EDGE('',*,*,#120,.F.); +#316 = PLANE('',#317); +#317 = AXIS2_PLACEMENT_3D('',#318,#319,#320); +#318 = CARTESIAN_POINT('',(0.,650.,1.15E+03)); +#319 = DIRECTION('',(0.,1.,0.)); +#320 = DIRECTION('',(0.,-0.,1.)); +#321 = ADVANCED_FACE('',(#322),#347,.F.); +#322 = FACE_BOUND('',#323,.F.); +#323 = EDGE_LOOP('',(#324,#334,#340,#341)); +#324 = ORIENTED_EDGE('',*,*,#325,.F.); +#325 = EDGE_CURVE('',#326,#328,#330,.T.); +#326 = VERTEX_POINT('',#327); +#327 = CARTESIAN_POINT('',(330.,605.,55.)); +#328 = VERTEX_POINT('',#329); +#329 = CARTESIAN_POINT('',(330.,605.,2.245E+03)); +#330 = LINE('',#331,#332); +#331 = CARTESIAN_POINT('',(330.,605.,0.)); +#332 = VECTOR('',#333,1.); +#333 = DIRECTION('',(0.,0.,1.)); +#334 = ORIENTED_EDGE('',*,*,#335,.F.); +#335 = EDGE_CURVE('',#295,#326,#336,.T.); +#336 = LINE('',#337,#338); +#337 = CARTESIAN_POINT('',(330.,600.,55.)); +#338 = VECTOR('',#339,1.); +#339 = DIRECTION('',(0.,-1.,0.)); +#340 = ORIENTED_EDGE('',*,*,#302,.T.); +#341 = ORIENTED_EDGE('',*,*,#342,.T.); +#342 = EDGE_CURVE('',#303,#328,#343,.T.); +#343 = LINE('',#344,#345); +#344 = CARTESIAN_POINT('',(330.,600.,2.245E+03)); +#345 = VECTOR('',#346,1.); +#346 = DIRECTION('',(0.,-1.,0.)); +#347 = PLANE('',#348); +#348 = AXIS2_PLACEMENT_3D('',#349,#350,#351); +#349 = CARTESIAN_POINT('',(330.,605.,0.)); +#350 = DIRECTION('',(1.,0.,-0.)); +#351 = DIRECTION('',(0.,0.,1.)); +#352 = ADVANCED_FACE('',(#353),#378,.F.); +#353 = FACE_BOUND('',#354,.F.); +#354 = EDGE_LOOP('',(#355,#363,#364,#372)); +#355 = ORIENTED_EDGE('',*,*,#356,.F.); +#356 = EDGE_CURVE('',#326,#357,#359,.T.); +#357 = VERTEX_POINT('',#358); +#358 = CARTESIAN_POINT('',(375.,605.,55.)); +#359 = LINE('',#360,#361); +#360 = CARTESIAN_POINT('',(-22.5,605.,55.)); +#361 = VECTOR('',#362,1.); +#362 = DIRECTION('',(1.,0.,-0.)); +#363 = ORIENTED_EDGE('',*,*,#325,.T.); +#364 = ORIENTED_EDGE('',*,*,#365,.T.); +#365 = EDGE_CURVE('',#328,#366,#368,.T.); +#366 = VERTEX_POINT('',#367); +#367 = CARTESIAN_POINT('',(375.,605.,2.245E+03)); +#368 = LINE('',#369,#370); +#369 = CARTESIAN_POINT('',(-22.5,605.,2.245E+03)); +#370 = VECTOR('',#371,1.); +#371 = DIRECTION('',(1.,0.,-0.)); +#372 = ORIENTED_EDGE('',*,*,#373,.F.); +#373 = EDGE_CURVE('',#357,#366,#374,.T.); +#374 = LINE('',#375,#376); +#375 = CARTESIAN_POINT('',(375.,605.,0.)); +#376 = VECTOR('',#377,1.); +#377 = DIRECTION('',(0.,0.,1.)); +#378 = PLANE('',#379); +#379 = AXIS2_PLACEMENT_3D('',#380,#381,#382); +#380 = CARTESIAN_POINT('',(330.,605.,0.)); +#381 = DIRECTION('',(-0.,1.,0.)); +#382 = DIRECTION('',(0.,0.,1.)); +#383 = ADVANCED_FACE('',(#384,#402),#436,.T.); +#384 = FACE_BOUND('',#385,.T.); +#385 = EDGE_LOOP('',(#386,#387,#388,#396)); +#386 = ORIENTED_EDGE('',*,*,#142,.T.); +#387 = ORIENTED_EDGE('',*,*,#286,.T.); +#388 = ORIENTED_EDGE('',*,*,#389,.F.); +#389 = EDGE_CURVE('',#390,#279,#392,.T.); +#390 = VERTEX_POINT('',#391); +#391 = CARTESIAN_POINT('',(375.,-650.,0.)); +#392 = LINE('',#393,#394); +#393 = CARTESIAN_POINT('',(375.,605.,0.)); +#394 = VECTOR('',#395,1.); +#395 = DIRECTION('',(-0.,1.,0.)); +#396 = ORIENTED_EDGE('',*,*,#397,.F.); +#397 = EDGE_CURVE('',#143,#390,#398,.T.); +#398 = LINE('',#399,#400); +#399 = CARTESIAN_POINT('',(330.,-650.,0.)); +#400 = VECTOR('',#401,1.); +#401 = DIRECTION('',(1.,0.,-0.)); +#402 = FACE_BOUND('',#403,.T.); +#403 = EDGE_LOOP('',(#404,#414,#422,#430)); +#404 = ORIENTED_EDGE('',*,*,#405,.F.); +#405 = EDGE_CURVE('',#406,#408,#410,.T.); +#406 = VERTEX_POINT('',#407); +#407 = CARTESIAN_POINT('',(-320.,595.,0.)); +#408 = VERTEX_POINT('',#409); +#409 = CARTESIAN_POINT('',(320.,595.,0.)); +#410 = LINE('',#411,#412); +#411 = CARTESIAN_POINT('',(-375.,595.,0.)); +#412 = VECTOR('',#413,1.); +#413 = DIRECTION('',(1.,0.,-0.)); +#414 = ORIENTED_EDGE('',*,*,#415,.F.); +#415 = EDGE_CURVE('',#416,#406,#418,.T.); +#416 = VERTEX_POINT('',#417); +#417 = CARTESIAN_POINT('',(-320.,-595.,0.)); +#418 = LINE('',#419,#420); +#419 = CARTESIAN_POINT('',(-320.,-650.,0.)); +#420 = VECTOR('',#421,1.); +#421 = DIRECTION('',(-0.,1.,0.)); +#422 = ORIENTED_EDGE('',*,*,#423,.T.); +#423 = EDGE_CURVE('',#416,#424,#426,.T.); +#424 = VERTEX_POINT('',#425); +#425 = CARTESIAN_POINT('',(320.,-595.,0.)); +#426 = LINE('',#427,#428); +#427 = CARTESIAN_POINT('',(-375.,-595.,0.)); +#428 = VECTOR('',#429,1.); +#429 = DIRECTION('',(1.,0.,-0.)); +#430 = ORIENTED_EDGE('',*,*,#431,.T.); +#431 = EDGE_CURVE('',#424,#408,#432,.T.); +#432 = LINE('',#433,#434); +#433 = CARTESIAN_POINT('',(320.,-650.,0.)); +#434 = VECTOR('',#435,1.); +#435 = DIRECTION('',(-0.,1.,0.)); +#436 = PLANE('',#437); +#437 = AXIS2_PLACEMENT_3D('',#438,#439,#440); +#438 = CARTESIAN_POINT('',(0.,0.,0.)); +#439 = DIRECTION('',(-0.,-0.,-1.)); +#440 = DIRECTION('',(-1.,0.,0.)); +#441 = ADVANCED_FACE('',(#442),#467,.F.); +#442 = FACE_BOUND('',#443,.F.); +#443 = EDGE_LOOP('',(#444,#445,#453,#461)); +#444 = ORIENTED_EDGE('',*,*,#405,.F.); +#445 = ORIENTED_EDGE('',*,*,#446,.F.); +#446 = EDGE_CURVE('',#447,#406,#449,.T.); +#447 = VERTEX_POINT('',#448); +#448 = CARTESIAN_POINT('',(-320.,595.,55.)); +#449 = LINE('',#450,#451); +#450 = CARTESIAN_POINT('',(-320.,595.,0.)); +#451 = VECTOR('',#452,1.); +#452 = DIRECTION('',(-0.,0.,-1.)); +#453 = ORIENTED_EDGE('',*,*,#454,.T.); +#454 = EDGE_CURVE('',#447,#455,#457,.T.); +#455 = VERTEX_POINT('',#456); +#456 = CARTESIAN_POINT('',(320.,595.,55.)); +#457 = LINE('',#458,#459); +#458 = CARTESIAN_POINT('',(-375.,595.,55.)); +#459 = VECTOR('',#460,1.); +#460 = DIRECTION('',(1.,0.,-0.)); +#461 = ORIENTED_EDGE('',*,*,#462,.T.); +#462 = EDGE_CURVE('',#455,#408,#463,.T.); +#463 = LINE('',#464,#465); +#464 = CARTESIAN_POINT('',(320.,595.,0.)); +#465 = VECTOR('',#466,1.); +#466 = DIRECTION('',(-0.,0.,-1.)); +#467 = PLANE('',#468); +#468 = AXIS2_PLACEMENT_3D('',#469,#470,#471); +#469 = CARTESIAN_POINT('',(-375.,595.,0.)); +#470 = DIRECTION('',(-0.,1.,0.)); +#471 = DIRECTION('',(0.,0.,1.)); +#472 = ADVANCED_FACE('',(#473),#491,.T.); +#473 = FACE_BOUND('',#474,.T.); +#474 = EDGE_LOOP('',(#475,#483,#484,#485)); +#475 = ORIENTED_EDGE('',*,*,#476,.T.); +#476 = EDGE_CURVE('',#477,#416,#479,.T.); +#477 = VERTEX_POINT('',#478); +#478 = CARTESIAN_POINT('',(-320.,-595.,55.)); +#479 = LINE('',#480,#481); +#480 = CARTESIAN_POINT('',(-320.,-595.,0.)); +#481 = VECTOR('',#482,1.); +#482 = DIRECTION('',(-0.,0.,-1.)); +#483 = ORIENTED_EDGE('',*,*,#415,.T.); +#484 = ORIENTED_EDGE('',*,*,#446,.F.); +#485 = ORIENTED_EDGE('',*,*,#486,.F.); +#486 = EDGE_CURVE('',#477,#447,#487,.T.); +#487 = LINE('',#488,#489); +#488 = CARTESIAN_POINT('',(-320.,-650.,55.)); +#489 = VECTOR('',#490,1.); +#490 = DIRECTION('',(-0.,1.,0.)); +#491 = PLANE('',#492); +#492 = AXIS2_PLACEMENT_3D('',#493,#494,#495); +#493 = CARTESIAN_POINT('',(-320.,-650.,0.)); +#494 = DIRECTION('',(1.,0.,-0.)); +#495 = DIRECTION('',(0.,0.,1.)); +#496 = ADVANCED_FACE('',(#497),#515,.F.); +#497 = FACE_BOUND('',#498,.F.); +#498 = EDGE_LOOP('',(#499,#507,#508,#509)); +#499 = ORIENTED_EDGE('',*,*,#500,.T.); +#500 = EDGE_CURVE('',#501,#424,#503,.T.); +#501 = VERTEX_POINT('',#502); +#502 = CARTESIAN_POINT('',(320.,-595.,55.)); +#503 = LINE('',#504,#505); +#504 = CARTESIAN_POINT('',(320.,-595.,0.)); +#505 = VECTOR('',#506,1.); +#506 = DIRECTION('',(-0.,0.,-1.)); +#507 = ORIENTED_EDGE('',*,*,#431,.T.); +#508 = ORIENTED_EDGE('',*,*,#462,.F.); +#509 = ORIENTED_EDGE('',*,*,#510,.F.); +#510 = EDGE_CURVE('',#501,#455,#511,.T.); +#511 = LINE('',#512,#513); +#512 = CARTESIAN_POINT('',(320.,-650.,55.)); +#513 = VECTOR('',#514,1.); +#514 = DIRECTION('',(-0.,1.,0.)); +#515 = PLANE('',#516); +#516 = AXIS2_PLACEMENT_3D('',#517,#518,#519); +#517 = CARTESIAN_POINT('',(320.,-650.,0.)); +#518 = DIRECTION('',(1.,0.,-0.)); +#519 = DIRECTION('',(0.,0.,1.)); +#520 = ADVANCED_FACE('',(#521),#532,.T.); +#521 = FACE_BOUND('',#522,.T.); +#522 = EDGE_LOOP('',(#523,#524,#525,#531)); +#523 = ORIENTED_EDGE('',*,*,#423,.F.); +#524 = ORIENTED_EDGE('',*,*,#476,.F.); +#525 = ORIENTED_EDGE('',*,*,#526,.T.); +#526 = EDGE_CURVE('',#477,#501,#527,.T.); +#527 = LINE('',#528,#529); +#528 = CARTESIAN_POINT('',(-375.,-595.,55.)); +#529 = VECTOR('',#530,1.); +#530 = DIRECTION('',(1.,0.,-0.)); +#531 = ORIENTED_EDGE('',*,*,#500,.T.); +#532 = PLANE('',#533); +#533 = AXIS2_PLACEMENT_3D('',#534,#535,#536); +#534 = CARTESIAN_POINT('',(-375.,-595.,0.)); +#535 = DIRECTION('',(-0.,1.,0.)); +#536 = DIRECTION('',(0.,0.,1.)); +#537 = ADVANCED_FACE('',(#538,#578),#584,.T.); +#538 = FACE_BOUND('',#539,.T.); +#539 = EDGE_LOOP('',(#540,#541,#542,#543,#551,#559,#567,#573,#574,#575, + #576,#577)); +#540 = ORIENTED_EDGE('',*,*,#193,.F.); +#541 = ORIENTED_EDGE('',*,*,#247,.T.); +#542 = ORIENTED_EDGE('',*,*,#217,.T.); +#543 = ORIENTED_EDGE('',*,*,#544,.T.); +#544 = EDGE_CURVE('',#208,#545,#547,.T.); +#545 = VERTEX_POINT('',#546); +#546 = CARTESIAN_POINT('',(330.,-650.,55.)); +#547 = LINE('',#548,#549); +#548 = CARTESIAN_POINT('',(-375.,-650.,55.)); +#549 = VECTOR('',#550,1.); +#550 = DIRECTION('',(1.,0.,-0.)); +#551 = ORIENTED_EDGE('',*,*,#552,.F.); +#552 = EDGE_CURVE('',#553,#545,#555,.T.); +#553 = VERTEX_POINT('',#554); +#554 = CARTESIAN_POINT('',(330.,-605.,55.)); +#555 = LINE('',#556,#557); +#556 = CARTESIAN_POINT('',(330.,-650.,55.)); +#557 = VECTOR('',#558,1.); +#558 = DIRECTION('',(0.,-1.,0.)); +#559 = ORIENTED_EDGE('',*,*,#560,.T.); +#560 = EDGE_CURVE('',#553,#561,#563,.T.); +#561 = VERTEX_POINT('',#562); +#562 = CARTESIAN_POINT('',(375.,-605.,55.)); +#563 = LINE('',#564,#565); +#564 = CARTESIAN_POINT('',(-22.5,-605.,55.)); +#565 = VECTOR('',#566,1.); +#566 = DIRECTION('',(1.,0.,-0.)); +#567 = ORIENTED_EDGE('',*,*,#568,.T.); +#568 = EDGE_CURVE('',#561,#357,#569,.T.); +#569 = LINE('',#570,#571); +#570 = CARTESIAN_POINT('',(375.,-650.,55.)); +#571 = VECTOR('',#572,1.); +#572 = DIRECTION('',(-0.,1.,0.)); +#573 = ORIENTED_EDGE('',*,*,#356,.F.); +#574 = ORIENTED_EDGE('',*,*,#335,.F.); +#575 = ORIENTED_EDGE('',*,*,#294,.F.); +#576 = ORIENTED_EDGE('',*,*,#112,.T.); +#577 = ORIENTED_EDGE('',*,*,#71,.F.); +#578 = FACE_BOUND('',#579,.T.); +#579 = EDGE_LOOP('',(#580,#581,#582,#583)); +#580 = ORIENTED_EDGE('',*,*,#454,.T.); +#581 = ORIENTED_EDGE('',*,*,#510,.F.); +#582 = ORIENTED_EDGE('',*,*,#526,.F.); +#583 = ORIENTED_EDGE('',*,*,#486,.T.); +#584 = PLANE('',#585); +#585 = AXIS2_PLACEMENT_3D('',#586,#587,#588); +#586 = CARTESIAN_POINT('',(0.,0.,55.)); +#587 = DIRECTION('',(0.,0.,1.)); +#588 = DIRECTION('',(1.,0.,-0.)); +#589 = ADVANCED_FACE('',(#590),#615,.T.); +#590 = FACE_BOUND('',#591,.T.); +#591 = EDGE_LOOP('',(#592,#593,#601,#609)); +#592 = ORIENTED_EDGE('',*,*,#560,.F.); +#593 = ORIENTED_EDGE('',*,*,#594,.T.); +#594 = EDGE_CURVE('',#553,#595,#597,.T.); +#595 = VERTEX_POINT('',#596); +#596 = CARTESIAN_POINT('',(330.,-605.,2.245E+03)); +#597 = LINE('',#598,#599); +#598 = CARTESIAN_POINT('',(330.,-605.,0.)); +#599 = VECTOR('',#600,1.); +#600 = DIRECTION('',(0.,0.,1.)); +#601 = ORIENTED_EDGE('',*,*,#602,.T.); +#602 = EDGE_CURVE('',#595,#603,#605,.T.); +#603 = VERTEX_POINT('',#604); +#604 = CARTESIAN_POINT('',(375.,-605.,2.245E+03)); +#605 = LINE('',#606,#607); +#606 = CARTESIAN_POINT('',(-22.5,-605.,2.245E+03)); +#607 = VECTOR('',#608,1.); +#608 = DIRECTION('',(1.,0.,-0.)); +#609 = ORIENTED_EDGE('',*,*,#610,.F.); +#610 = EDGE_CURVE('',#561,#603,#611,.T.); +#611 = LINE('',#612,#613); +#612 = CARTESIAN_POINT('',(375.,-605.,0.)); +#613 = VECTOR('',#614,1.); +#614 = DIRECTION('',(0.,0.,1.)); +#615 = PLANE('',#616); +#616 = AXIS2_PLACEMENT_3D('',#617,#618,#619); +#617 = CARTESIAN_POINT('',(330.,-605.,0.)); +#618 = DIRECTION('',(-0.,1.,0.)); +#619 = DIRECTION('',(0.,0.,1.)); +#620 = ADVANCED_FACE('',(#621),#639,.F.); +#621 = FACE_BOUND('',#622,.F.); +#622 = EDGE_LOOP('',(#623,#631,#632,#633)); +#623 = ORIENTED_EDGE('',*,*,#624,.F.); +#624 = EDGE_CURVE('',#545,#625,#627,.T.); +#625 = VERTEX_POINT('',#626); +#626 = CARTESIAN_POINT('',(330.,-650.,2.245E+03)); +#627 = LINE('',#628,#629); +#628 = CARTESIAN_POINT('',(330.,-650.,0.)); +#629 = VECTOR('',#630,1.); +#630 = DIRECTION('',(0.,0.,1.)); +#631 = ORIENTED_EDGE('',*,*,#552,.F.); +#632 = ORIENTED_EDGE('',*,*,#594,.T.); +#633 = ORIENTED_EDGE('',*,*,#634,.T.); +#634 = EDGE_CURVE('',#595,#625,#635,.T.); +#635 = LINE('',#636,#637); +#636 = CARTESIAN_POINT('',(330.,-650.,2.245E+03)); +#637 = VECTOR('',#638,1.); +#638 = DIRECTION('',(0.,-1.,0.)); +#639 = PLANE('',#640); +#640 = AXIS2_PLACEMENT_3D('',#641,#642,#643); +#641 = CARTESIAN_POINT('',(330.,-650.,0.)); +#642 = DIRECTION('',(1.,0.,-0.)); +#643 = DIRECTION('',(0.,0.,1.)); +#644 = ADVANCED_FACE('',(#645,#669),#703,.T.); +#645 = FACE_BOUND('',#646,.T.); +#646 = EDGE_LOOP('',(#647,#648,#649,#650,#651,#652,#653,#659,#660,#661, + #667,#668)); +#647 = ORIENTED_EDGE('',*,*,#177,.T.); +#648 = ORIENTED_EDGE('',*,*,#89,.T.); +#649 = ORIENTED_EDGE('',*,*,#128,.F.); +#650 = ORIENTED_EDGE('',*,*,#310,.T.); +#651 = ORIENTED_EDGE('',*,*,#342,.T.); +#652 = ORIENTED_EDGE('',*,*,#365,.T.); +#653 = ORIENTED_EDGE('',*,*,#654,.F.); +#654 = EDGE_CURVE('',#603,#366,#655,.T.); +#655 = LINE('',#656,#657); +#656 = CARTESIAN_POINT('',(375.,595.,2.245E+03)); +#657 = VECTOR('',#658,1.); +#658 = DIRECTION('',(-0.,1.,0.)); +#659 = ORIENTED_EDGE('',*,*,#602,.F.); +#660 = ORIENTED_EDGE('',*,*,#634,.T.); +#661 = ORIENTED_EDGE('',*,*,#662,.F.); +#662 = EDGE_CURVE('',#210,#625,#663,.T.); +#663 = LINE('',#664,#665); +#664 = CARTESIAN_POINT('',(-375.,-650.,2.245E+03)); +#665 = VECTOR('',#666,1.); +#666 = DIRECTION('',(1.,0.,-0.)); +#667 = ORIENTED_EDGE('',*,*,#233,.F.); +#668 = ORIENTED_EDGE('',*,*,#254,.F.); +#669 = FACE_BOUND('',#670,.T.); +#670 = EDGE_LOOP('',(#671,#681,#689,#697)); +#671 = ORIENTED_EDGE('',*,*,#672,.F.); +#672 = EDGE_CURVE('',#673,#675,#677,.T.); +#673 = VERTEX_POINT('',#674); +#674 = CARTESIAN_POINT('',(-320.,595.,2.245E+03)); +#675 = VERTEX_POINT('',#676); +#676 = CARTESIAN_POINT('',(320.,595.,2.245E+03)); +#677 = LINE('',#678,#679); +#678 = CARTESIAN_POINT('',(-375.,595.,2.245E+03)); +#679 = VECTOR('',#680,1.); +#680 = DIRECTION('',(1.,0.,-0.)); +#681 = ORIENTED_EDGE('',*,*,#682,.F.); +#682 = EDGE_CURVE('',#683,#673,#685,.T.); +#683 = VERTEX_POINT('',#684); +#684 = CARTESIAN_POINT('',(-320.,-595.,2.245E+03)); +#685 = LINE('',#686,#687); +#686 = CARTESIAN_POINT('',(-320.,-650.,2.245E+03)); +#687 = VECTOR('',#688,1.); +#688 = DIRECTION('',(-0.,1.,0.)); +#689 = ORIENTED_EDGE('',*,*,#690,.T.); +#690 = EDGE_CURVE('',#683,#691,#693,.T.); +#691 = VERTEX_POINT('',#692); +#692 = CARTESIAN_POINT('',(320.,-595.,2.245E+03)); +#693 = LINE('',#694,#695); +#694 = CARTESIAN_POINT('',(-375.,-595.,2.245E+03)); +#695 = VECTOR('',#696,1.); +#696 = DIRECTION('',(1.,0.,-0.)); +#697 = ORIENTED_EDGE('',*,*,#698,.T.); +#698 = EDGE_CURVE('',#691,#675,#699,.T.); +#699 = LINE('',#700,#701); +#700 = CARTESIAN_POINT('',(320.,-650.,2.245E+03)); +#701 = VECTOR('',#702,1.); +#702 = DIRECTION('',(-0.,1.,0.)); +#703 = PLANE('',#704); +#704 = AXIS2_PLACEMENT_3D('',#705,#706,#707); +#705 = CARTESIAN_POINT('',(0.,0.,2.245E+03)); +#706 = DIRECTION('',(-0.,-0.,-1.)); +#707 = DIRECTION('',(-1.,0.,0.)); +#708 = ADVANCED_FACE('',(#709),#734,.F.); +#709 = FACE_BOUND('',#710,.F.); +#710 = EDGE_LOOP('',(#711,#712,#720,#728)); +#711 = ORIENTED_EDGE('',*,*,#672,.F.); +#712 = ORIENTED_EDGE('',*,*,#713,.F.); +#713 = EDGE_CURVE('',#714,#673,#716,.T.); +#714 = VERTEX_POINT('',#715); +#715 = CARTESIAN_POINT('',(-320.,595.,2.3E+03)); +#716 = LINE('',#717,#718); +#717 = CARTESIAN_POINT('',(-320.,595.,2.245E+03)); +#718 = VECTOR('',#719,1.); +#719 = DIRECTION('',(-0.,0.,-1.)); +#720 = ORIENTED_EDGE('',*,*,#721,.T.); +#721 = EDGE_CURVE('',#714,#722,#724,.T.); +#722 = VERTEX_POINT('',#723); +#723 = CARTESIAN_POINT('',(320.,595.,2.3E+03)); +#724 = LINE('',#725,#726); +#725 = CARTESIAN_POINT('',(-375.,595.,2.3E+03)); +#726 = VECTOR('',#727,1.); +#727 = DIRECTION('',(1.,0.,-0.)); +#728 = ORIENTED_EDGE('',*,*,#729,.T.); +#729 = EDGE_CURVE('',#722,#675,#730,.T.); +#730 = LINE('',#731,#732); +#731 = CARTESIAN_POINT('',(320.,595.,2.245E+03)); +#732 = VECTOR('',#733,1.); +#733 = DIRECTION('',(-0.,0.,-1.)); +#734 = PLANE('',#735); +#735 = AXIS2_PLACEMENT_3D('',#736,#737,#738); +#736 = CARTESIAN_POINT('',(-375.,595.,2.245E+03)); +#737 = DIRECTION('',(-0.,1.,0.)); +#738 = DIRECTION('',(0.,0.,1.)); +#739 = ADVANCED_FACE('',(#740),#758,.T.); +#740 = FACE_BOUND('',#741,.T.); +#741 = EDGE_LOOP('',(#742,#750,#751,#752)); +#742 = ORIENTED_EDGE('',*,*,#743,.T.); +#743 = EDGE_CURVE('',#744,#683,#746,.T.); +#744 = VERTEX_POINT('',#745); +#745 = CARTESIAN_POINT('',(-320.,-595.,2.3E+03)); +#746 = LINE('',#747,#748); +#747 = CARTESIAN_POINT('',(-320.,-595.,2.245E+03)); +#748 = VECTOR('',#749,1.); +#749 = DIRECTION('',(-0.,0.,-1.)); +#750 = ORIENTED_EDGE('',*,*,#682,.T.); +#751 = ORIENTED_EDGE('',*,*,#713,.F.); +#752 = ORIENTED_EDGE('',*,*,#753,.F.); +#753 = EDGE_CURVE('',#744,#714,#754,.T.); +#754 = LINE('',#755,#756); +#755 = CARTESIAN_POINT('',(-320.,-650.,2.3E+03)); +#756 = VECTOR('',#757,1.); +#757 = DIRECTION('',(-0.,1.,0.)); +#758 = PLANE('',#759); +#759 = AXIS2_PLACEMENT_3D('',#760,#761,#762); +#760 = CARTESIAN_POINT('',(-320.,-650.,2.245E+03)); +#761 = DIRECTION('',(1.,0.,-0.)); +#762 = DIRECTION('',(0.,0.,1.)); +#763 = ADVANCED_FACE('',(#764),#782,.F.); +#764 = FACE_BOUND('',#765,.F.); +#765 = EDGE_LOOP('',(#766,#774,#775,#776)); +#766 = ORIENTED_EDGE('',*,*,#767,.T.); +#767 = EDGE_CURVE('',#768,#691,#770,.T.); +#768 = VERTEX_POINT('',#769); +#769 = CARTESIAN_POINT('',(320.,-595.,2.3E+03)); +#770 = LINE('',#771,#772); +#771 = CARTESIAN_POINT('',(320.,-595.,2.245E+03)); +#772 = VECTOR('',#773,1.); +#773 = DIRECTION('',(-0.,0.,-1.)); +#774 = ORIENTED_EDGE('',*,*,#698,.T.); +#775 = ORIENTED_EDGE('',*,*,#729,.F.); +#776 = ORIENTED_EDGE('',*,*,#777,.F.); +#777 = EDGE_CURVE('',#768,#722,#778,.T.); +#778 = LINE('',#779,#780); +#779 = CARTESIAN_POINT('',(320.,-650.,2.3E+03)); +#780 = VECTOR('',#781,1.); +#781 = DIRECTION('',(-0.,1.,0.)); +#782 = PLANE('',#783); +#783 = AXIS2_PLACEMENT_3D('',#784,#785,#786); +#784 = CARTESIAN_POINT('',(320.,-650.,2.245E+03)); +#785 = DIRECTION('',(1.,0.,-0.)); +#786 = DIRECTION('',(0.,0.,1.)); +#787 = ADVANCED_FACE('',(#788),#799,.T.); +#788 = FACE_BOUND('',#789,.T.); +#789 = EDGE_LOOP('',(#790,#791,#792,#798)); +#790 = ORIENTED_EDGE('',*,*,#690,.F.); +#791 = ORIENTED_EDGE('',*,*,#743,.F.); +#792 = ORIENTED_EDGE('',*,*,#793,.T.); +#793 = EDGE_CURVE('',#744,#768,#794,.T.); +#794 = LINE('',#795,#796); +#795 = CARTESIAN_POINT('',(-375.,-595.,2.3E+03)); +#796 = VECTOR('',#797,1.); +#797 = DIRECTION('',(1.,0.,-0.)); +#798 = ORIENTED_EDGE('',*,*,#767,.T.); +#799 = PLANE('',#800); +#800 = AXIS2_PLACEMENT_3D('',#801,#802,#803); +#801 = CARTESIAN_POINT('',(-375.,-595.,2.245E+03)); +#802 = DIRECTION('',(-0.,1.,0.)); +#803 = DIRECTION('',(0.,0.,1.)); +#804 = ADVANCED_FACE('',(#805,#823),#829,.T.); +#805 = FACE_BOUND('',#806,.T.); +#806 = EDGE_LOOP('',(#807,#808,#816,#822)); +#807 = ORIENTED_EDGE('',*,*,#160,.F.); +#808 = ORIENTED_EDGE('',*,*,#809,.T.); +#809 = EDGE_CURVE('',#153,#810,#812,.T.); +#810 = VERTEX_POINT('',#811); +#811 = CARTESIAN_POINT('',(375.,-650.,2.3E+03)); +#812 = LINE('',#813,#814); +#813 = CARTESIAN_POINT('',(-375.,-650.,2.3E+03)); +#814 = VECTOR('',#815,1.); +#815 = DIRECTION('',(1.,0.,-0.)); +#816 = ORIENTED_EDGE('',*,*,#817,.T.); +#817 = EDGE_CURVE('',#810,#271,#818,.T.); +#818 = LINE('',#819,#820); +#819 = CARTESIAN_POINT('',(375.,-650.,2.3E+03)); +#820 = VECTOR('',#821,1.); +#821 = DIRECTION('',(-0.,1.,0.)); +#822 = ORIENTED_EDGE('',*,*,#270,.F.); +#823 = FACE_BOUND('',#824,.T.); +#824 = EDGE_LOOP('',(#825,#826,#827,#828)); +#825 = ORIENTED_EDGE('',*,*,#721,.T.); +#826 = ORIENTED_EDGE('',*,*,#777,.F.); +#827 = ORIENTED_EDGE('',*,*,#793,.F.); +#828 = ORIENTED_EDGE('',*,*,#753,.T.); +#829 = PLANE('',#830); +#830 = AXIS2_PLACEMENT_3D('',#831,#832,#833); +#831 = CARTESIAN_POINT('',(0.,0.,2.3E+03)); +#832 = DIRECTION('',(0.,0.,1.)); +#833 = DIRECTION('',(1.,0.,-0.)); +#834 = ADVANCED_FACE('',(#835,#846),#852,.T.); +#835 = FACE_BOUND('',#836,.T.); +#836 = EDGE_LOOP('',(#837,#838,#839,#840)); +#837 = ORIENTED_EDGE('',*,*,#389,.T.); +#838 = ORIENTED_EDGE('',*,*,#278,.T.); +#839 = ORIENTED_EDGE('',*,*,#817,.F.); +#840 = ORIENTED_EDGE('',*,*,#841,.F.); +#841 = EDGE_CURVE('',#390,#810,#842,.T.); +#842 = LINE('',#843,#844); +#843 = CARTESIAN_POINT('',(375.,-650.,0.)); +#844 = VECTOR('',#845,1.); +#845 = DIRECTION('',(0.,0.,1.)); +#846 = FACE_BOUND('',#847,.T.); +#847 = EDGE_LOOP('',(#848,#849,#850,#851)); +#848 = ORIENTED_EDGE('',*,*,#568,.F.); +#849 = ORIENTED_EDGE('',*,*,#610,.T.); +#850 = ORIENTED_EDGE('',*,*,#654,.T.); +#851 = ORIENTED_EDGE('',*,*,#373,.F.); +#852 = PLANE('',#853); +#853 = AXIS2_PLACEMENT_3D('',#854,#855,#856); +#854 = CARTESIAN_POINT('',(375.,0.,1.15E+03)); +#855 = DIRECTION('',(1.,0.,0.)); +#856 = DIRECTION('',(-0.,0.,1.)); +#857 = ADVANCED_FACE('',(#858,#864),#870,.T.); +#858 = FACE_BOUND('',#859,.T.); +#859 = EDGE_LOOP('',(#860,#861,#862,#863)); +#860 = ORIENTED_EDGE('',*,*,#397,.T.); +#861 = ORIENTED_EDGE('',*,*,#841,.T.); +#862 = ORIENTED_EDGE('',*,*,#809,.F.); +#863 = ORIENTED_EDGE('',*,*,#152,.F.); +#864 = FACE_BOUND('',#865,.T.); +#865 = EDGE_LOOP('',(#866,#867,#868,#869)); +#866 = ORIENTED_EDGE('',*,*,#544,.F.); +#867 = ORIENTED_EDGE('',*,*,#207,.T.); +#868 = ORIENTED_EDGE('',*,*,#662,.T.); +#869 = ORIENTED_EDGE('',*,*,#624,.F.); +#870 = PLANE('',#871); +#871 = AXIS2_PLACEMENT_3D('',#872,#873,#874); +#872 = CARTESIAN_POINT('',(0.,-650.,1.15E+03)); +#873 = DIRECTION('',(-0.,-1.,-0.)); +#874 = DIRECTION('',(0.,0.,-1.)); +#875 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#879)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#876,#877,#878)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#876 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#877 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#878 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#879 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#876, + 'distance_accuracy_value','confusion accuracy'); +#880 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#881,#883); +#881 = ( REPRESENTATION_RELATIONSHIP('','',#64,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#882) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#882 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15); +#883 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#884 + ); +#884 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','NAU03_Cabinet_Frame','',#5, + #59,$); +#885 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#61)); +#886 = SHAPE_DEFINITION_REPRESENTATION(#887,#893); +#887 = PRODUCT_DEFINITION_SHAPE('','',#888); +#888 = PRODUCT_DEFINITION('design','',#889,#892); +#889 = PRODUCT_DEFINITION_FORMATION('','',#890); +#890 = PRODUCT('NAU03_Left_Side_Panel','NAU03_Left_Side_Panel','',(#891) + ); +#891 = PRODUCT_CONTEXT('',#2,'mechanical'); +#892 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#893 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#894),#2404); +#894 = MANIFOLD_SOLID_BREP('',#895); +#895 = CLOSED_SHELL('',(#896,#936,#1307,#1331,#1695,#1712,#1724,#1746, + #1763,#1780,#1792,#1814,#1831,#1848,#1860,#1882,#1899,#1916,#1928, + #1950,#1967,#1984,#1996,#2018,#2035,#2052,#2064,#2086,#2103,#2120, + #2132,#2154,#2171,#2188,#2200,#2222,#2239,#2256,#2268,#2290,#2307, + #2324,#2336,#2358,#2375,#2392)); +#896 = ADVANCED_FACE('',(#897),#931,.F.); +#897 = FACE_BOUND('',#898,.F.); +#898 = EDGE_LOOP('',(#899,#909,#917,#925)); +#899 = ORIENTED_EDGE('',*,*,#900,.F.); +#900 = EDGE_CURVE('',#901,#903,#905,.T.); +#901 = VERTEX_POINT('',#902); +#902 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#903 = VERTEX_POINT('',#904); +#904 = CARTESIAN_POINT('',(-407.,-608.,2.25E+03)); +#905 = LINE('',#906,#907); +#906 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#907 = VECTOR('',#908,1.); +#908 = DIRECTION('',(0.,0.,1.)); +#909 = ORIENTED_EDGE('',*,*,#910,.T.); +#910 = EDGE_CURVE('',#901,#911,#913,.T.); +#911 = VERTEX_POINT('',#912); +#912 = CARTESIAN_POINT('',(-407.,608.,50.)); +#913 = LINE('',#914,#915); +#914 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#915 = VECTOR('',#916,1.); +#916 = DIRECTION('',(-0.,1.,0.)); +#917 = ORIENTED_EDGE('',*,*,#918,.T.); +#918 = EDGE_CURVE('',#911,#919,#921,.T.); +#919 = VERTEX_POINT('',#920); +#920 = CARTESIAN_POINT('',(-407.,608.,2.25E+03)); +#921 = LINE('',#922,#923); +#922 = CARTESIAN_POINT('',(-407.,608.,50.)); +#923 = VECTOR('',#924,1.); +#924 = DIRECTION('',(0.,0.,1.)); +#925 = ORIENTED_EDGE('',*,*,#926,.F.); +#926 = EDGE_CURVE('',#903,#919,#927,.T.); +#927 = LINE('',#928,#929); +#928 = CARTESIAN_POINT('',(-407.,-608.,2.25E+03)); +#929 = VECTOR('',#930,1.); +#930 = DIRECTION('',(-0.,1.,0.)); +#931 = PLANE('',#932); +#932 = AXIS2_PLACEMENT_3D('',#933,#934,#935); +#933 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#934 = DIRECTION('',(1.,0.,-0.)); +#935 = DIRECTION('',(0.,0.,1.)); +#936 = ADVANCED_FACE('',(#937,#962,#996,#1030,#1064,#1098,#1132,#1166, + #1200,#1234,#1268),#1302,.F.); +#937 = FACE_BOUND('',#938,.F.); +#938 = EDGE_LOOP('',(#939,#947,#948,#956)); +#939 = ORIENTED_EDGE('',*,*,#940,.F.); +#940 = EDGE_CURVE('',#901,#941,#943,.T.); +#941 = VERTEX_POINT('',#942); +#942 = CARTESIAN_POINT('',(-375.,-608.,50.)); +#943 = LINE('',#944,#945); +#944 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#945 = VECTOR('',#946,1.); +#946 = DIRECTION('',(1.,0.,-0.)); +#947 = ORIENTED_EDGE('',*,*,#900,.T.); +#948 = ORIENTED_EDGE('',*,*,#949,.T.); +#949 = EDGE_CURVE('',#903,#950,#952,.T.); +#950 = VERTEX_POINT('',#951); +#951 = CARTESIAN_POINT('',(-375.,-608.,2.25E+03)); +#952 = LINE('',#953,#954); +#953 = CARTESIAN_POINT('',(-407.,-608.,2.25E+03)); +#954 = VECTOR('',#955,1.); +#955 = DIRECTION('',(1.,0.,-0.)); +#956 = ORIENTED_EDGE('',*,*,#957,.F.); +#957 = EDGE_CURVE('',#941,#950,#958,.T.); +#958 = LINE('',#959,#960); +#959 = CARTESIAN_POINT('',(-375.,-608.,50.)); +#960 = VECTOR('',#961,1.); +#961 = DIRECTION('',(0.,0.,1.)); +#962 = FACE_BOUND('',#963,.F.); +#963 = EDGE_LOOP('',(#964,#974,#982,#990)); +#964 = ORIENTED_EDGE('',*,*,#965,.F.); +#965 = EDGE_CURVE('',#966,#968,#970,.T.); +#966 = VERTEX_POINT('',#967); +#967 = CARTESIAN_POINT('',(-400.92,-608.,550.)); +#968 = VERTEX_POINT('',#969); +#969 = CARTESIAN_POINT('',(-381.08,-608.,550.)); +#970 = LINE('',#971,#972); +#971 = CARTESIAN_POINT('',(-403.96,-608.,550.)); +#972 = VECTOR('',#973,1.); +#973 = DIRECTION('',(1.,0.,-0.)); +#974 = ORIENTED_EDGE('',*,*,#975,.T.); +#975 = EDGE_CURVE('',#966,#976,#978,.T.); +#976 = VERTEX_POINT('',#977); +#977 = CARTESIAN_POINT('',(-400.92,-608.,534.)); +#978 = LINE('',#979,#980); +#979 = CARTESIAN_POINT('',(-400.92,-608.,292.)); +#980 = VECTOR('',#981,1.); +#981 = DIRECTION('',(-0.,0.,-1.)); +#982 = ORIENTED_EDGE('',*,*,#983,.T.); +#983 = EDGE_CURVE('',#976,#984,#986,.T.); +#984 = VERTEX_POINT('',#985); +#985 = CARTESIAN_POINT('',(-381.08,-608.,534.)); +#986 = LINE('',#987,#988); +#987 = CARTESIAN_POINT('',(-403.96,-608.,534.)); +#988 = VECTOR('',#989,1.); +#989 = DIRECTION('',(1.,0.,-0.)); +#990 = ORIENTED_EDGE('',*,*,#991,.F.); +#991 = EDGE_CURVE('',#968,#984,#992,.T.); +#992 = LINE('',#993,#994); +#993 = CARTESIAN_POINT('',(-381.08,-608.,292.)); +#994 = VECTOR('',#995,1.); +#995 = DIRECTION('',(-0.,0.,-1.)); +#996 = FACE_BOUND('',#997,.F.); +#997 = EDGE_LOOP('',(#998,#1008,#1016,#1024)); +#998 = ORIENTED_EDGE('',*,*,#999,.F.); +#999 = EDGE_CURVE('',#1000,#1002,#1004,.T.); +#1000 = VERTEX_POINT('',#1001); +#1001 = CARTESIAN_POINT('',(-400.92,-608.,592.)); +#1002 = VERTEX_POINT('',#1003); +#1003 = CARTESIAN_POINT('',(-381.08,-608.,592.)); +#1004 = LINE('',#1005,#1006); +#1005 = CARTESIAN_POINT('',(-403.96,-608.,592.)); +#1006 = VECTOR('',#1007,1.); +#1007 = DIRECTION('',(1.,0.,-0.)); +#1008 = ORIENTED_EDGE('',*,*,#1009,.T.); +#1009 = EDGE_CURVE('',#1000,#1010,#1012,.T.); +#1010 = VERTEX_POINT('',#1011); +#1011 = CARTESIAN_POINT('',(-400.92,-608.,576.)); +#1012 = LINE('',#1013,#1014); +#1013 = CARTESIAN_POINT('',(-400.92,-608.,313.)); +#1014 = VECTOR('',#1015,1.); +#1015 = DIRECTION('',(-0.,0.,-1.)); +#1016 = ORIENTED_EDGE('',*,*,#1017,.T.); +#1017 = EDGE_CURVE('',#1010,#1018,#1020,.T.); +#1018 = VERTEX_POINT('',#1019); +#1019 = CARTESIAN_POINT('',(-381.08,-608.,576.)); +#1020 = LINE('',#1021,#1022); +#1021 = CARTESIAN_POINT('',(-403.96,-608.,576.)); +#1022 = VECTOR('',#1023,1.); +#1023 = DIRECTION('',(1.,0.,-0.)); +#1024 = ORIENTED_EDGE('',*,*,#1025,.F.); +#1025 = EDGE_CURVE('',#1002,#1018,#1026,.T.); +#1026 = LINE('',#1027,#1028); +#1027 = CARTESIAN_POINT('',(-381.08,-608.,313.)); +#1028 = VECTOR('',#1029,1.); +#1029 = DIRECTION('',(-0.,0.,-1.)); +#1030 = FACE_BOUND('',#1031,.F.); +#1031 = EDGE_LOOP('',(#1032,#1042,#1050,#1058)); +#1032 = ORIENTED_EDGE('',*,*,#1033,.F.); +#1033 = EDGE_CURVE('',#1034,#1036,#1038,.T.); +#1034 = VERTEX_POINT('',#1035); +#1035 = CARTESIAN_POINT('',(-400.92,-608.,634.)); +#1036 = VERTEX_POINT('',#1037); +#1037 = CARTESIAN_POINT('',(-381.08,-608.,634.)); +#1038 = LINE('',#1039,#1040); +#1039 = CARTESIAN_POINT('',(-403.96,-608.,634.)); +#1040 = VECTOR('',#1041,1.); +#1041 = DIRECTION('',(1.,0.,-0.)); +#1042 = ORIENTED_EDGE('',*,*,#1043,.T.); +#1043 = EDGE_CURVE('',#1034,#1044,#1046,.T.); +#1044 = VERTEX_POINT('',#1045); +#1045 = CARTESIAN_POINT('',(-400.92,-608.,618.)); +#1046 = LINE('',#1047,#1048); +#1047 = CARTESIAN_POINT('',(-400.92,-608.,334.)); +#1048 = VECTOR('',#1049,1.); +#1049 = DIRECTION('',(-0.,0.,-1.)); +#1050 = ORIENTED_EDGE('',*,*,#1051,.T.); +#1051 = EDGE_CURVE('',#1044,#1052,#1054,.T.); +#1052 = VERTEX_POINT('',#1053); +#1053 = CARTESIAN_POINT('',(-381.08,-608.,618.)); +#1054 = LINE('',#1055,#1056); +#1055 = CARTESIAN_POINT('',(-403.96,-608.,618.)); +#1056 = VECTOR('',#1057,1.); +#1057 = DIRECTION('',(1.,0.,-0.)); +#1058 = ORIENTED_EDGE('',*,*,#1059,.F.); +#1059 = EDGE_CURVE('',#1036,#1052,#1060,.T.); +#1060 = LINE('',#1061,#1062); +#1061 = CARTESIAN_POINT('',(-381.08,-608.,334.)); +#1062 = VECTOR('',#1063,1.); +#1063 = DIRECTION('',(-0.,0.,-1.)); +#1064 = FACE_BOUND('',#1065,.F.); +#1065 = EDGE_LOOP('',(#1066,#1076,#1084,#1092)); +#1066 = ORIENTED_EDGE('',*,*,#1067,.F.); +#1067 = EDGE_CURVE('',#1068,#1070,#1072,.T.); +#1068 = VERTEX_POINT('',#1069); +#1069 = CARTESIAN_POINT('',(-400.92,-608.,676.)); +#1070 = VERTEX_POINT('',#1071); +#1071 = CARTESIAN_POINT('',(-381.08,-608.,676.)); +#1072 = LINE('',#1073,#1074); +#1073 = CARTESIAN_POINT('',(-403.96,-608.,676.)); +#1074 = VECTOR('',#1075,1.); +#1075 = DIRECTION('',(1.,0.,-0.)); +#1076 = ORIENTED_EDGE('',*,*,#1077,.T.); +#1077 = EDGE_CURVE('',#1068,#1078,#1080,.T.); +#1078 = VERTEX_POINT('',#1079); +#1079 = CARTESIAN_POINT('',(-400.92,-608.,660.)); +#1080 = LINE('',#1081,#1082); +#1081 = CARTESIAN_POINT('',(-400.92,-608.,355.)); +#1082 = VECTOR('',#1083,1.); +#1083 = DIRECTION('',(-0.,0.,-1.)); +#1084 = ORIENTED_EDGE('',*,*,#1085,.T.); +#1085 = EDGE_CURVE('',#1078,#1086,#1088,.T.); +#1086 = VERTEX_POINT('',#1087); +#1087 = CARTESIAN_POINT('',(-381.08,-608.,660.)); +#1088 = LINE('',#1089,#1090); +#1089 = CARTESIAN_POINT('',(-403.96,-608.,660.)); +#1090 = VECTOR('',#1091,1.); +#1091 = DIRECTION('',(1.,0.,-0.)); +#1092 = ORIENTED_EDGE('',*,*,#1093,.F.); +#1093 = EDGE_CURVE('',#1070,#1086,#1094,.T.); +#1094 = LINE('',#1095,#1096); +#1095 = CARTESIAN_POINT('',(-381.08,-608.,355.)); +#1096 = VECTOR('',#1097,1.); +#1097 = DIRECTION('',(-0.,0.,-1.)); +#1098 = FACE_BOUND('',#1099,.F.); +#1099 = EDGE_LOOP('',(#1100,#1110,#1118,#1126)); +#1100 = ORIENTED_EDGE('',*,*,#1101,.F.); +#1101 = EDGE_CURVE('',#1102,#1104,#1106,.T.); +#1102 = VERTEX_POINT('',#1103); +#1103 = CARTESIAN_POINT('',(-400.92,-608.,718.)); +#1104 = VERTEX_POINT('',#1105); +#1105 = CARTESIAN_POINT('',(-381.08,-608.,718.)); +#1106 = LINE('',#1107,#1108); +#1107 = CARTESIAN_POINT('',(-403.96,-608.,718.)); +#1108 = VECTOR('',#1109,1.); +#1109 = DIRECTION('',(1.,0.,-0.)); +#1110 = ORIENTED_EDGE('',*,*,#1111,.T.); +#1111 = EDGE_CURVE('',#1102,#1112,#1114,.T.); +#1112 = VERTEX_POINT('',#1113); +#1113 = CARTESIAN_POINT('',(-400.92,-608.,702.)); +#1114 = LINE('',#1115,#1116); +#1115 = CARTESIAN_POINT('',(-400.92,-608.,376.)); +#1116 = VECTOR('',#1117,1.); +#1117 = DIRECTION('',(-0.,0.,-1.)); +#1118 = ORIENTED_EDGE('',*,*,#1119,.T.); +#1119 = EDGE_CURVE('',#1112,#1120,#1122,.T.); +#1120 = VERTEX_POINT('',#1121); +#1121 = CARTESIAN_POINT('',(-381.08,-608.,702.)); +#1122 = LINE('',#1123,#1124); +#1123 = CARTESIAN_POINT('',(-403.96,-608.,702.)); +#1124 = VECTOR('',#1125,1.); +#1125 = DIRECTION('',(1.,0.,-0.)); +#1126 = ORIENTED_EDGE('',*,*,#1127,.F.); +#1127 = EDGE_CURVE('',#1104,#1120,#1128,.T.); +#1128 = LINE('',#1129,#1130); +#1129 = CARTESIAN_POINT('',(-381.08,-608.,376.)); +#1130 = VECTOR('',#1131,1.); +#1131 = DIRECTION('',(-0.,0.,-1.)); +#1132 = FACE_BOUND('',#1133,.F.); +#1133 = EDGE_LOOP('',(#1134,#1144,#1152,#1160)); +#1134 = ORIENTED_EDGE('',*,*,#1135,.F.); +#1135 = EDGE_CURVE('',#1136,#1138,#1140,.T.); +#1136 = VERTEX_POINT('',#1137); +#1137 = CARTESIAN_POINT('',(-400.92,-608.,760.)); +#1138 = VERTEX_POINT('',#1139); +#1139 = CARTESIAN_POINT('',(-381.08,-608.,760.)); +#1140 = LINE('',#1141,#1142); +#1141 = CARTESIAN_POINT('',(-403.96,-608.,760.)); +#1142 = VECTOR('',#1143,1.); +#1143 = DIRECTION('',(1.,0.,-0.)); +#1144 = ORIENTED_EDGE('',*,*,#1145,.T.); +#1145 = EDGE_CURVE('',#1136,#1146,#1148,.T.); +#1146 = VERTEX_POINT('',#1147); +#1147 = CARTESIAN_POINT('',(-400.92,-608.,744.)); +#1148 = LINE('',#1149,#1150); +#1149 = CARTESIAN_POINT('',(-400.92,-608.,397.)); +#1150 = VECTOR('',#1151,1.); +#1151 = DIRECTION('',(-0.,0.,-1.)); +#1152 = ORIENTED_EDGE('',*,*,#1153,.T.); +#1153 = EDGE_CURVE('',#1146,#1154,#1156,.T.); +#1154 = VERTEX_POINT('',#1155); +#1155 = CARTESIAN_POINT('',(-381.08,-608.,744.)); +#1156 = LINE('',#1157,#1158); +#1157 = CARTESIAN_POINT('',(-403.96,-608.,744.)); +#1158 = VECTOR('',#1159,1.); +#1159 = DIRECTION('',(1.,0.,-0.)); +#1160 = ORIENTED_EDGE('',*,*,#1161,.F.); +#1161 = EDGE_CURVE('',#1138,#1154,#1162,.T.); +#1162 = LINE('',#1163,#1164); +#1163 = CARTESIAN_POINT('',(-381.08,-608.,397.)); +#1164 = VECTOR('',#1165,1.); +#1165 = DIRECTION('',(-0.,0.,-1.)); +#1166 = FACE_BOUND('',#1167,.F.); +#1167 = EDGE_LOOP('',(#1168,#1178,#1186,#1194)); +#1168 = ORIENTED_EDGE('',*,*,#1169,.F.); +#1169 = EDGE_CURVE('',#1170,#1172,#1174,.T.); +#1170 = VERTEX_POINT('',#1171); +#1171 = CARTESIAN_POINT('',(-400.92,-608.,802.)); +#1172 = VERTEX_POINT('',#1173); +#1173 = CARTESIAN_POINT('',(-381.08,-608.,802.)); +#1174 = LINE('',#1175,#1176); +#1175 = CARTESIAN_POINT('',(-403.96,-608.,802.)); +#1176 = VECTOR('',#1177,1.); +#1177 = DIRECTION('',(1.,0.,-0.)); +#1178 = ORIENTED_EDGE('',*,*,#1179,.T.); +#1179 = EDGE_CURVE('',#1170,#1180,#1182,.T.); +#1180 = VERTEX_POINT('',#1181); +#1181 = CARTESIAN_POINT('',(-400.92,-608.,786.)); +#1182 = LINE('',#1183,#1184); +#1183 = CARTESIAN_POINT('',(-400.92,-608.,418.)); +#1184 = VECTOR('',#1185,1.); +#1185 = DIRECTION('',(-0.,0.,-1.)); +#1186 = ORIENTED_EDGE('',*,*,#1187,.T.); +#1187 = EDGE_CURVE('',#1180,#1188,#1190,.T.); +#1188 = VERTEX_POINT('',#1189); +#1189 = CARTESIAN_POINT('',(-381.08,-608.,786.)); +#1190 = LINE('',#1191,#1192); +#1191 = CARTESIAN_POINT('',(-403.96,-608.,786.)); +#1192 = VECTOR('',#1193,1.); +#1193 = DIRECTION('',(1.,0.,-0.)); +#1194 = ORIENTED_EDGE('',*,*,#1195,.F.); +#1195 = EDGE_CURVE('',#1172,#1188,#1196,.T.); +#1196 = LINE('',#1197,#1198); +#1197 = CARTESIAN_POINT('',(-381.08,-608.,418.)); +#1198 = VECTOR('',#1199,1.); +#1199 = DIRECTION('',(-0.,0.,-1.)); +#1200 = FACE_BOUND('',#1201,.F.); +#1201 = EDGE_LOOP('',(#1202,#1212,#1220,#1228)); +#1202 = ORIENTED_EDGE('',*,*,#1203,.F.); +#1203 = EDGE_CURVE('',#1204,#1206,#1208,.T.); +#1204 = VERTEX_POINT('',#1205); +#1205 = CARTESIAN_POINT('',(-400.92,-608.,844.)); +#1206 = VERTEX_POINT('',#1207); +#1207 = CARTESIAN_POINT('',(-381.08,-608.,844.)); +#1208 = LINE('',#1209,#1210); +#1209 = CARTESIAN_POINT('',(-403.96,-608.,844.)); +#1210 = VECTOR('',#1211,1.); +#1211 = DIRECTION('',(1.,0.,-0.)); +#1212 = ORIENTED_EDGE('',*,*,#1213,.T.); +#1213 = EDGE_CURVE('',#1204,#1214,#1216,.T.); +#1214 = VERTEX_POINT('',#1215); +#1215 = CARTESIAN_POINT('',(-400.92,-608.,828.)); +#1216 = LINE('',#1217,#1218); +#1217 = CARTESIAN_POINT('',(-400.92,-608.,439.)); +#1218 = VECTOR('',#1219,1.); +#1219 = DIRECTION('',(-0.,0.,-1.)); +#1220 = ORIENTED_EDGE('',*,*,#1221,.T.); +#1221 = EDGE_CURVE('',#1214,#1222,#1224,.T.); +#1222 = VERTEX_POINT('',#1223); +#1223 = CARTESIAN_POINT('',(-381.08,-608.,828.)); +#1224 = LINE('',#1225,#1226); +#1225 = CARTESIAN_POINT('',(-403.96,-608.,828.)); +#1226 = VECTOR('',#1227,1.); +#1227 = DIRECTION('',(1.,0.,-0.)); +#1228 = ORIENTED_EDGE('',*,*,#1229,.F.); +#1229 = EDGE_CURVE('',#1206,#1222,#1230,.T.); +#1230 = LINE('',#1231,#1232); +#1231 = CARTESIAN_POINT('',(-381.08,-608.,439.)); +#1232 = VECTOR('',#1233,1.); +#1233 = DIRECTION('',(-0.,0.,-1.)); +#1234 = FACE_BOUND('',#1235,.F.); +#1235 = EDGE_LOOP('',(#1236,#1246,#1254,#1262)); +#1236 = ORIENTED_EDGE('',*,*,#1237,.F.); +#1237 = EDGE_CURVE('',#1238,#1240,#1242,.T.); +#1238 = VERTEX_POINT('',#1239); +#1239 = CARTESIAN_POINT('',(-400.92,-608.,886.)); +#1240 = VERTEX_POINT('',#1241); +#1241 = CARTESIAN_POINT('',(-381.08,-608.,886.)); +#1242 = LINE('',#1243,#1244); +#1243 = CARTESIAN_POINT('',(-403.96,-608.,886.)); +#1244 = VECTOR('',#1245,1.); +#1245 = DIRECTION('',(1.,0.,-0.)); +#1246 = ORIENTED_EDGE('',*,*,#1247,.T.); +#1247 = EDGE_CURVE('',#1238,#1248,#1250,.T.); +#1248 = VERTEX_POINT('',#1249); +#1249 = CARTESIAN_POINT('',(-400.92,-608.,870.)); +#1250 = LINE('',#1251,#1252); +#1251 = CARTESIAN_POINT('',(-400.92,-608.,460.)); +#1252 = VECTOR('',#1253,1.); +#1253 = DIRECTION('',(-0.,0.,-1.)); +#1254 = ORIENTED_EDGE('',*,*,#1255,.T.); +#1255 = EDGE_CURVE('',#1248,#1256,#1258,.T.); +#1256 = VERTEX_POINT('',#1257); +#1257 = CARTESIAN_POINT('',(-381.08,-608.,870.)); +#1258 = LINE('',#1259,#1260); +#1259 = CARTESIAN_POINT('',(-403.96,-608.,870.)); +#1260 = VECTOR('',#1261,1.); +#1261 = DIRECTION('',(1.,0.,-0.)); +#1262 = ORIENTED_EDGE('',*,*,#1263,.F.); +#1263 = EDGE_CURVE('',#1240,#1256,#1264,.T.); +#1264 = LINE('',#1265,#1266); +#1265 = CARTESIAN_POINT('',(-381.08,-608.,460.)); +#1266 = VECTOR('',#1267,1.); +#1267 = DIRECTION('',(-0.,0.,-1.)); +#1268 = FACE_BOUND('',#1269,.F.); +#1269 = EDGE_LOOP('',(#1270,#1280,#1288,#1296)); +#1270 = ORIENTED_EDGE('',*,*,#1271,.F.); +#1271 = EDGE_CURVE('',#1272,#1274,#1276,.T.); +#1272 = VERTEX_POINT('',#1273); +#1273 = CARTESIAN_POINT('',(-400.92,-608.,928.)); +#1274 = VERTEX_POINT('',#1275); +#1275 = CARTESIAN_POINT('',(-381.08,-608.,928.)); +#1276 = LINE('',#1277,#1278); +#1277 = CARTESIAN_POINT('',(-403.96,-608.,928.)); +#1278 = VECTOR('',#1279,1.); +#1279 = DIRECTION('',(1.,0.,-0.)); +#1280 = ORIENTED_EDGE('',*,*,#1281,.T.); +#1281 = EDGE_CURVE('',#1272,#1282,#1284,.T.); +#1282 = VERTEX_POINT('',#1283); +#1283 = CARTESIAN_POINT('',(-400.92,-608.,912.)); +#1284 = LINE('',#1285,#1286); +#1285 = CARTESIAN_POINT('',(-400.92,-608.,481.)); +#1286 = VECTOR('',#1287,1.); +#1287 = DIRECTION('',(-0.,0.,-1.)); +#1288 = ORIENTED_EDGE('',*,*,#1289,.T.); +#1289 = EDGE_CURVE('',#1282,#1290,#1292,.T.); +#1290 = VERTEX_POINT('',#1291); +#1291 = CARTESIAN_POINT('',(-381.08,-608.,912.)); +#1292 = LINE('',#1293,#1294); +#1293 = CARTESIAN_POINT('',(-403.96,-608.,912.)); +#1294 = VECTOR('',#1295,1.); +#1295 = DIRECTION('',(1.,0.,-0.)); +#1296 = ORIENTED_EDGE('',*,*,#1297,.F.); +#1297 = EDGE_CURVE('',#1274,#1290,#1298,.T.); +#1298 = LINE('',#1299,#1300); +#1299 = CARTESIAN_POINT('',(-381.08,-608.,481.)); +#1300 = VECTOR('',#1301,1.); +#1301 = DIRECTION('',(-0.,0.,-1.)); +#1302 = PLANE('',#1303); +#1303 = AXIS2_PLACEMENT_3D('',#1304,#1305,#1306); +#1304 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#1305 = DIRECTION('',(-0.,1.,0.)); +#1306 = DIRECTION('',(0.,0.,1.)); +#1307 = ADVANCED_FACE('',(#1308),#1326,.T.); +#1308 = FACE_BOUND('',#1309,.T.); +#1309 = EDGE_LOOP('',(#1310,#1311,#1312,#1320)); +#1310 = ORIENTED_EDGE('',*,*,#926,.F.); +#1311 = ORIENTED_EDGE('',*,*,#949,.T.); +#1312 = ORIENTED_EDGE('',*,*,#1313,.T.); +#1313 = EDGE_CURVE('',#950,#1314,#1316,.T.); +#1314 = VERTEX_POINT('',#1315); +#1315 = CARTESIAN_POINT('',(-375.,608.,2.25E+03)); +#1316 = LINE('',#1317,#1318); +#1317 = CARTESIAN_POINT('',(-375.,-608.,2.25E+03)); +#1318 = VECTOR('',#1319,1.); +#1319 = DIRECTION('',(-0.,1.,0.)); +#1320 = ORIENTED_EDGE('',*,*,#1321,.F.); +#1321 = EDGE_CURVE('',#919,#1314,#1322,.T.); +#1322 = LINE('',#1323,#1324); +#1323 = CARTESIAN_POINT('',(-407.,608.,2.25E+03)); +#1324 = VECTOR('',#1325,1.); +#1325 = DIRECTION('',(1.,0.,-0.)); +#1326 = PLANE('',#1327); +#1327 = AXIS2_PLACEMENT_3D('',#1328,#1329,#1330); +#1328 = CARTESIAN_POINT('',(-407.,-608.,2.25E+03)); +#1329 = DIRECTION('',(0.,0.,1.)); +#1330 = DIRECTION('',(1.,0.,-0.)); +#1331 = ADVANCED_FACE('',(#1332,#1350,#1384,#1418,#1452,#1486,#1520, + #1554,#1588,#1622,#1656),#1690,.T.); +#1332 = FACE_BOUND('',#1333,.T.); +#1333 = EDGE_LOOP('',(#1334,#1342,#1343,#1344)); +#1334 = ORIENTED_EDGE('',*,*,#1335,.F.); +#1335 = EDGE_CURVE('',#911,#1336,#1338,.T.); +#1336 = VERTEX_POINT('',#1337); +#1337 = CARTESIAN_POINT('',(-375.,608.,50.)); +#1338 = LINE('',#1339,#1340); +#1339 = CARTESIAN_POINT('',(-407.,608.,50.)); +#1340 = VECTOR('',#1341,1.); +#1341 = DIRECTION('',(1.,0.,-0.)); +#1342 = ORIENTED_EDGE('',*,*,#918,.T.); +#1343 = ORIENTED_EDGE('',*,*,#1321,.T.); +#1344 = ORIENTED_EDGE('',*,*,#1345,.F.); +#1345 = EDGE_CURVE('',#1336,#1314,#1346,.T.); +#1346 = LINE('',#1347,#1348); +#1347 = CARTESIAN_POINT('',(-375.,608.,50.)); +#1348 = VECTOR('',#1349,1.); +#1349 = DIRECTION('',(0.,0.,1.)); +#1350 = FACE_BOUND('',#1351,.T.); +#1351 = EDGE_LOOP('',(#1352,#1362,#1370,#1378)); +#1352 = ORIENTED_EDGE('',*,*,#1353,.F.); +#1353 = EDGE_CURVE('',#1354,#1356,#1358,.T.); +#1354 = VERTEX_POINT('',#1355); +#1355 = CARTESIAN_POINT('',(-400.92,608.,550.)); +#1356 = VERTEX_POINT('',#1357); +#1357 = CARTESIAN_POINT('',(-381.08,608.,550.)); +#1358 = LINE('',#1359,#1360); +#1359 = CARTESIAN_POINT('',(-403.96,608.,550.)); +#1360 = VECTOR('',#1361,1.); +#1361 = DIRECTION('',(1.,0.,-0.)); +#1362 = ORIENTED_EDGE('',*,*,#1363,.T.); +#1363 = EDGE_CURVE('',#1354,#1364,#1366,.T.); +#1364 = VERTEX_POINT('',#1365); +#1365 = CARTESIAN_POINT('',(-400.92,608.,534.)); +#1366 = LINE('',#1367,#1368); +#1367 = CARTESIAN_POINT('',(-400.92,608.,292.)); +#1368 = VECTOR('',#1369,1.); +#1369 = DIRECTION('',(-0.,0.,-1.)); +#1370 = ORIENTED_EDGE('',*,*,#1371,.T.); +#1371 = EDGE_CURVE('',#1364,#1372,#1374,.T.); +#1372 = VERTEX_POINT('',#1373); +#1373 = CARTESIAN_POINT('',(-381.08,608.,534.)); +#1374 = LINE('',#1375,#1376); +#1375 = CARTESIAN_POINT('',(-403.96,608.,534.)); +#1376 = VECTOR('',#1377,1.); +#1377 = DIRECTION('',(1.,0.,-0.)); +#1378 = ORIENTED_EDGE('',*,*,#1379,.F.); +#1379 = EDGE_CURVE('',#1356,#1372,#1380,.T.); +#1380 = LINE('',#1381,#1382); +#1381 = CARTESIAN_POINT('',(-381.08,608.,292.)); +#1382 = VECTOR('',#1383,1.); +#1383 = DIRECTION('',(-0.,0.,-1.)); +#1384 = FACE_BOUND('',#1385,.T.); +#1385 = EDGE_LOOP('',(#1386,#1396,#1404,#1412)); +#1386 = ORIENTED_EDGE('',*,*,#1387,.F.); +#1387 = EDGE_CURVE('',#1388,#1390,#1392,.T.); +#1388 = VERTEX_POINT('',#1389); +#1389 = CARTESIAN_POINT('',(-400.92,608.,592.)); +#1390 = VERTEX_POINT('',#1391); +#1391 = CARTESIAN_POINT('',(-381.08,608.,592.)); +#1392 = LINE('',#1393,#1394); +#1393 = CARTESIAN_POINT('',(-403.96,608.,592.)); +#1394 = VECTOR('',#1395,1.); +#1395 = DIRECTION('',(1.,0.,-0.)); +#1396 = ORIENTED_EDGE('',*,*,#1397,.T.); +#1397 = EDGE_CURVE('',#1388,#1398,#1400,.T.); +#1398 = VERTEX_POINT('',#1399); +#1399 = CARTESIAN_POINT('',(-400.92,608.,576.)); +#1400 = LINE('',#1401,#1402); +#1401 = CARTESIAN_POINT('',(-400.92,608.,313.)); +#1402 = VECTOR('',#1403,1.); +#1403 = DIRECTION('',(-0.,0.,-1.)); +#1404 = ORIENTED_EDGE('',*,*,#1405,.T.); +#1405 = EDGE_CURVE('',#1398,#1406,#1408,.T.); +#1406 = VERTEX_POINT('',#1407); +#1407 = CARTESIAN_POINT('',(-381.08,608.,576.)); +#1408 = LINE('',#1409,#1410); +#1409 = CARTESIAN_POINT('',(-403.96,608.,576.)); +#1410 = VECTOR('',#1411,1.); +#1411 = DIRECTION('',(1.,0.,-0.)); +#1412 = ORIENTED_EDGE('',*,*,#1413,.F.); +#1413 = EDGE_CURVE('',#1390,#1406,#1414,.T.); +#1414 = LINE('',#1415,#1416); +#1415 = CARTESIAN_POINT('',(-381.08,608.,313.)); +#1416 = VECTOR('',#1417,1.); +#1417 = DIRECTION('',(-0.,0.,-1.)); +#1418 = FACE_BOUND('',#1419,.T.); +#1419 = EDGE_LOOP('',(#1420,#1430,#1438,#1446)); +#1420 = ORIENTED_EDGE('',*,*,#1421,.F.); +#1421 = EDGE_CURVE('',#1422,#1424,#1426,.T.); +#1422 = VERTEX_POINT('',#1423); +#1423 = CARTESIAN_POINT('',(-400.92,608.,634.)); +#1424 = VERTEX_POINT('',#1425); +#1425 = CARTESIAN_POINT('',(-381.08,608.,634.)); +#1426 = LINE('',#1427,#1428); +#1427 = CARTESIAN_POINT('',(-403.96,608.,634.)); +#1428 = VECTOR('',#1429,1.); +#1429 = DIRECTION('',(1.,0.,-0.)); +#1430 = ORIENTED_EDGE('',*,*,#1431,.T.); +#1431 = EDGE_CURVE('',#1422,#1432,#1434,.T.); +#1432 = VERTEX_POINT('',#1433); +#1433 = CARTESIAN_POINT('',(-400.92,608.,618.)); +#1434 = LINE('',#1435,#1436); +#1435 = CARTESIAN_POINT('',(-400.92,608.,334.)); +#1436 = VECTOR('',#1437,1.); +#1437 = DIRECTION('',(-0.,0.,-1.)); +#1438 = ORIENTED_EDGE('',*,*,#1439,.T.); +#1439 = EDGE_CURVE('',#1432,#1440,#1442,.T.); +#1440 = VERTEX_POINT('',#1441); +#1441 = CARTESIAN_POINT('',(-381.08,608.,618.)); +#1442 = LINE('',#1443,#1444); +#1443 = CARTESIAN_POINT('',(-403.96,608.,618.)); +#1444 = VECTOR('',#1445,1.); +#1445 = DIRECTION('',(1.,0.,-0.)); +#1446 = ORIENTED_EDGE('',*,*,#1447,.F.); +#1447 = EDGE_CURVE('',#1424,#1440,#1448,.T.); +#1448 = LINE('',#1449,#1450); +#1449 = CARTESIAN_POINT('',(-381.08,608.,334.)); +#1450 = VECTOR('',#1451,1.); +#1451 = DIRECTION('',(-0.,0.,-1.)); +#1452 = FACE_BOUND('',#1453,.T.); +#1453 = EDGE_LOOP('',(#1454,#1464,#1472,#1480)); +#1454 = ORIENTED_EDGE('',*,*,#1455,.F.); +#1455 = EDGE_CURVE('',#1456,#1458,#1460,.T.); +#1456 = VERTEX_POINT('',#1457); +#1457 = CARTESIAN_POINT('',(-400.92,608.,676.)); +#1458 = VERTEX_POINT('',#1459); +#1459 = CARTESIAN_POINT('',(-381.08,608.,676.)); +#1460 = LINE('',#1461,#1462); +#1461 = CARTESIAN_POINT('',(-403.96,608.,676.)); +#1462 = VECTOR('',#1463,1.); +#1463 = DIRECTION('',(1.,0.,-0.)); +#1464 = ORIENTED_EDGE('',*,*,#1465,.T.); +#1465 = EDGE_CURVE('',#1456,#1466,#1468,.T.); +#1466 = VERTEX_POINT('',#1467); +#1467 = CARTESIAN_POINT('',(-400.92,608.,660.)); +#1468 = LINE('',#1469,#1470); +#1469 = CARTESIAN_POINT('',(-400.92,608.,355.)); +#1470 = VECTOR('',#1471,1.); +#1471 = DIRECTION('',(-0.,0.,-1.)); +#1472 = ORIENTED_EDGE('',*,*,#1473,.T.); +#1473 = EDGE_CURVE('',#1466,#1474,#1476,.T.); +#1474 = VERTEX_POINT('',#1475); +#1475 = CARTESIAN_POINT('',(-381.08,608.,660.)); +#1476 = LINE('',#1477,#1478); +#1477 = CARTESIAN_POINT('',(-403.96,608.,660.)); +#1478 = VECTOR('',#1479,1.); +#1479 = DIRECTION('',(1.,0.,-0.)); +#1480 = ORIENTED_EDGE('',*,*,#1481,.F.); +#1481 = EDGE_CURVE('',#1458,#1474,#1482,.T.); +#1482 = LINE('',#1483,#1484); +#1483 = CARTESIAN_POINT('',(-381.08,608.,355.)); +#1484 = VECTOR('',#1485,1.); +#1485 = DIRECTION('',(-0.,0.,-1.)); +#1486 = FACE_BOUND('',#1487,.T.); +#1487 = EDGE_LOOP('',(#1488,#1498,#1506,#1514)); +#1488 = ORIENTED_EDGE('',*,*,#1489,.F.); +#1489 = EDGE_CURVE('',#1490,#1492,#1494,.T.); +#1490 = VERTEX_POINT('',#1491); +#1491 = CARTESIAN_POINT('',(-400.92,608.,718.)); +#1492 = VERTEX_POINT('',#1493); +#1493 = CARTESIAN_POINT('',(-381.08,608.,718.)); +#1494 = LINE('',#1495,#1496); +#1495 = CARTESIAN_POINT('',(-403.96,608.,718.)); +#1496 = VECTOR('',#1497,1.); +#1497 = DIRECTION('',(1.,0.,-0.)); +#1498 = ORIENTED_EDGE('',*,*,#1499,.T.); +#1499 = EDGE_CURVE('',#1490,#1500,#1502,.T.); +#1500 = VERTEX_POINT('',#1501); +#1501 = CARTESIAN_POINT('',(-400.92,608.,702.)); +#1502 = LINE('',#1503,#1504); +#1503 = CARTESIAN_POINT('',(-400.92,608.,376.)); +#1504 = VECTOR('',#1505,1.); +#1505 = DIRECTION('',(-0.,0.,-1.)); +#1506 = ORIENTED_EDGE('',*,*,#1507,.T.); +#1507 = EDGE_CURVE('',#1500,#1508,#1510,.T.); +#1508 = VERTEX_POINT('',#1509); +#1509 = CARTESIAN_POINT('',(-381.08,608.,702.)); +#1510 = LINE('',#1511,#1512); +#1511 = CARTESIAN_POINT('',(-403.96,608.,702.)); +#1512 = VECTOR('',#1513,1.); +#1513 = DIRECTION('',(1.,0.,-0.)); +#1514 = ORIENTED_EDGE('',*,*,#1515,.F.); +#1515 = EDGE_CURVE('',#1492,#1508,#1516,.T.); +#1516 = LINE('',#1517,#1518); +#1517 = CARTESIAN_POINT('',(-381.08,608.,376.)); +#1518 = VECTOR('',#1519,1.); +#1519 = DIRECTION('',(-0.,0.,-1.)); +#1520 = FACE_BOUND('',#1521,.T.); +#1521 = EDGE_LOOP('',(#1522,#1532,#1540,#1548)); +#1522 = ORIENTED_EDGE('',*,*,#1523,.F.); +#1523 = EDGE_CURVE('',#1524,#1526,#1528,.T.); +#1524 = VERTEX_POINT('',#1525); +#1525 = CARTESIAN_POINT('',(-400.92,608.,760.)); +#1526 = VERTEX_POINT('',#1527); +#1527 = CARTESIAN_POINT('',(-381.08,608.,760.)); +#1528 = LINE('',#1529,#1530); +#1529 = CARTESIAN_POINT('',(-403.96,608.,760.)); +#1530 = VECTOR('',#1531,1.); +#1531 = DIRECTION('',(1.,0.,-0.)); +#1532 = ORIENTED_EDGE('',*,*,#1533,.T.); +#1533 = EDGE_CURVE('',#1524,#1534,#1536,.T.); +#1534 = VERTEX_POINT('',#1535); +#1535 = CARTESIAN_POINT('',(-400.92,608.,744.)); +#1536 = LINE('',#1537,#1538); +#1537 = CARTESIAN_POINT('',(-400.92,608.,397.)); +#1538 = VECTOR('',#1539,1.); +#1539 = DIRECTION('',(-0.,0.,-1.)); +#1540 = ORIENTED_EDGE('',*,*,#1541,.T.); +#1541 = EDGE_CURVE('',#1534,#1542,#1544,.T.); +#1542 = VERTEX_POINT('',#1543); +#1543 = CARTESIAN_POINT('',(-381.08,608.,744.)); +#1544 = LINE('',#1545,#1546); +#1545 = CARTESIAN_POINT('',(-403.96,608.,744.)); +#1546 = VECTOR('',#1547,1.); +#1547 = DIRECTION('',(1.,0.,-0.)); +#1548 = ORIENTED_EDGE('',*,*,#1549,.F.); +#1549 = EDGE_CURVE('',#1526,#1542,#1550,.T.); +#1550 = LINE('',#1551,#1552); +#1551 = CARTESIAN_POINT('',(-381.08,608.,397.)); +#1552 = VECTOR('',#1553,1.); +#1553 = DIRECTION('',(-0.,0.,-1.)); +#1554 = FACE_BOUND('',#1555,.T.); +#1555 = EDGE_LOOP('',(#1556,#1566,#1574,#1582)); +#1556 = ORIENTED_EDGE('',*,*,#1557,.F.); +#1557 = EDGE_CURVE('',#1558,#1560,#1562,.T.); +#1558 = VERTEX_POINT('',#1559); +#1559 = CARTESIAN_POINT('',(-400.92,608.,802.)); +#1560 = VERTEX_POINT('',#1561); +#1561 = CARTESIAN_POINT('',(-381.08,608.,802.)); +#1562 = LINE('',#1563,#1564); +#1563 = CARTESIAN_POINT('',(-403.96,608.,802.)); +#1564 = VECTOR('',#1565,1.); +#1565 = DIRECTION('',(1.,0.,-0.)); +#1566 = ORIENTED_EDGE('',*,*,#1567,.T.); +#1567 = EDGE_CURVE('',#1558,#1568,#1570,.T.); +#1568 = VERTEX_POINT('',#1569); +#1569 = CARTESIAN_POINT('',(-400.92,608.,786.)); +#1570 = LINE('',#1571,#1572); +#1571 = CARTESIAN_POINT('',(-400.92,608.,418.)); +#1572 = VECTOR('',#1573,1.); +#1573 = DIRECTION('',(-0.,0.,-1.)); +#1574 = ORIENTED_EDGE('',*,*,#1575,.T.); +#1575 = EDGE_CURVE('',#1568,#1576,#1578,.T.); +#1576 = VERTEX_POINT('',#1577); +#1577 = CARTESIAN_POINT('',(-381.08,608.,786.)); +#1578 = LINE('',#1579,#1580); +#1579 = CARTESIAN_POINT('',(-403.96,608.,786.)); +#1580 = VECTOR('',#1581,1.); +#1581 = DIRECTION('',(1.,0.,-0.)); +#1582 = ORIENTED_EDGE('',*,*,#1583,.F.); +#1583 = EDGE_CURVE('',#1560,#1576,#1584,.T.); +#1584 = LINE('',#1585,#1586); +#1585 = CARTESIAN_POINT('',(-381.08,608.,418.)); +#1586 = VECTOR('',#1587,1.); +#1587 = DIRECTION('',(-0.,0.,-1.)); +#1588 = FACE_BOUND('',#1589,.T.); +#1589 = EDGE_LOOP('',(#1590,#1600,#1608,#1616)); +#1590 = ORIENTED_EDGE('',*,*,#1591,.F.); +#1591 = EDGE_CURVE('',#1592,#1594,#1596,.T.); +#1592 = VERTEX_POINT('',#1593); +#1593 = CARTESIAN_POINT('',(-400.92,608.,844.)); +#1594 = VERTEX_POINT('',#1595); +#1595 = CARTESIAN_POINT('',(-381.08,608.,844.)); +#1596 = LINE('',#1597,#1598); +#1597 = CARTESIAN_POINT('',(-403.96,608.,844.)); +#1598 = VECTOR('',#1599,1.); +#1599 = DIRECTION('',(1.,0.,-0.)); +#1600 = ORIENTED_EDGE('',*,*,#1601,.T.); +#1601 = EDGE_CURVE('',#1592,#1602,#1604,.T.); +#1602 = VERTEX_POINT('',#1603); +#1603 = CARTESIAN_POINT('',(-400.92,608.,828.)); +#1604 = LINE('',#1605,#1606); +#1605 = CARTESIAN_POINT('',(-400.92,608.,439.)); +#1606 = VECTOR('',#1607,1.); +#1607 = DIRECTION('',(-0.,0.,-1.)); +#1608 = ORIENTED_EDGE('',*,*,#1609,.T.); +#1609 = EDGE_CURVE('',#1602,#1610,#1612,.T.); +#1610 = VERTEX_POINT('',#1611); +#1611 = CARTESIAN_POINT('',(-381.08,608.,828.)); +#1612 = LINE('',#1613,#1614); +#1613 = CARTESIAN_POINT('',(-403.96,608.,828.)); +#1614 = VECTOR('',#1615,1.); +#1615 = DIRECTION('',(1.,0.,-0.)); +#1616 = ORIENTED_EDGE('',*,*,#1617,.F.); +#1617 = EDGE_CURVE('',#1594,#1610,#1618,.T.); +#1618 = LINE('',#1619,#1620); +#1619 = CARTESIAN_POINT('',(-381.08,608.,439.)); +#1620 = VECTOR('',#1621,1.); +#1621 = DIRECTION('',(-0.,0.,-1.)); +#1622 = FACE_BOUND('',#1623,.T.); +#1623 = EDGE_LOOP('',(#1624,#1634,#1642,#1650)); +#1624 = ORIENTED_EDGE('',*,*,#1625,.F.); +#1625 = EDGE_CURVE('',#1626,#1628,#1630,.T.); +#1626 = VERTEX_POINT('',#1627); +#1627 = CARTESIAN_POINT('',(-400.92,608.,886.)); +#1628 = VERTEX_POINT('',#1629); +#1629 = CARTESIAN_POINT('',(-381.08,608.,886.)); +#1630 = LINE('',#1631,#1632); +#1631 = CARTESIAN_POINT('',(-403.96,608.,886.)); +#1632 = VECTOR('',#1633,1.); +#1633 = DIRECTION('',(1.,0.,-0.)); +#1634 = ORIENTED_EDGE('',*,*,#1635,.T.); +#1635 = EDGE_CURVE('',#1626,#1636,#1638,.T.); +#1636 = VERTEX_POINT('',#1637); +#1637 = CARTESIAN_POINT('',(-400.92,608.,870.)); +#1638 = LINE('',#1639,#1640); +#1639 = CARTESIAN_POINT('',(-400.92,608.,460.)); +#1640 = VECTOR('',#1641,1.); +#1641 = DIRECTION('',(-0.,0.,-1.)); +#1642 = ORIENTED_EDGE('',*,*,#1643,.T.); +#1643 = EDGE_CURVE('',#1636,#1644,#1646,.T.); +#1644 = VERTEX_POINT('',#1645); +#1645 = CARTESIAN_POINT('',(-381.08,608.,870.)); +#1646 = LINE('',#1647,#1648); +#1647 = CARTESIAN_POINT('',(-403.96,608.,870.)); +#1648 = VECTOR('',#1649,1.); +#1649 = DIRECTION('',(1.,0.,-0.)); +#1650 = ORIENTED_EDGE('',*,*,#1651,.F.); +#1651 = EDGE_CURVE('',#1628,#1644,#1652,.T.); +#1652 = LINE('',#1653,#1654); +#1653 = CARTESIAN_POINT('',(-381.08,608.,460.)); +#1654 = VECTOR('',#1655,1.); +#1655 = DIRECTION('',(-0.,0.,-1.)); +#1656 = FACE_BOUND('',#1657,.T.); +#1657 = EDGE_LOOP('',(#1658,#1668,#1676,#1684)); +#1658 = ORIENTED_EDGE('',*,*,#1659,.F.); +#1659 = EDGE_CURVE('',#1660,#1662,#1664,.T.); +#1660 = VERTEX_POINT('',#1661); +#1661 = CARTESIAN_POINT('',(-400.92,608.,928.)); +#1662 = VERTEX_POINT('',#1663); +#1663 = CARTESIAN_POINT('',(-381.08,608.,928.)); +#1664 = LINE('',#1665,#1666); +#1665 = CARTESIAN_POINT('',(-403.96,608.,928.)); +#1666 = VECTOR('',#1667,1.); +#1667 = DIRECTION('',(1.,0.,-0.)); +#1668 = ORIENTED_EDGE('',*,*,#1669,.T.); +#1669 = EDGE_CURVE('',#1660,#1670,#1672,.T.); +#1670 = VERTEX_POINT('',#1671); +#1671 = CARTESIAN_POINT('',(-400.92,608.,912.)); +#1672 = LINE('',#1673,#1674); +#1673 = CARTESIAN_POINT('',(-400.92,608.,481.)); +#1674 = VECTOR('',#1675,1.); +#1675 = DIRECTION('',(-0.,0.,-1.)); +#1676 = ORIENTED_EDGE('',*,*,#1677,.T.); +#1677 = EDGE_CURVE('',#1670,#1678,#1680,.T.); +#1678 = VERTEX_POINT('',#1679); +#1679 = CARTESIAN_POINT('',(-381.08,608.,912.)); +#1680 = LINE('',#1681,#1682); +#1681 = CARTESIAN_POINT('',(-403.96,608.,912.)); +#1682 = VECTOR('',#1683,1.); +#1683 = DIRECTION('',(1.,0.,-0.)); +#1684 = ORIENTED_EDGE('',*,*,#1685,.F.); +#1685 = EDGE_CURVE('',#1662,#1678,#1686,.T.); +#1686 = LINE('',#1687,#1688); +#1687 = CARTESIAN_POINT('',(-381.08,608.,481.)); +#1688 = VECTOR('',#1689,1.); +#1689 = DIRECTION('',(-0.,0.,-1.)); +#1690 = PLANE('',#1691); +#1691 = AXIS2_PLACEMENT_3D('',#1692,#1693,#1694); +#1692 = CARTESIAN_POINT('',(-407.,608.,50.)); +#1693 = DIRECTION('',(-0.,1.,0.)); +#1694 = DIRECTION('',(0.,0.,1.)); +#1695 = ADVANCED_FACE('',(#1696),#1707,.F.); +#1696 = FACE_BOUND('',#1697,.F.); +#1697 = EDGE_LOOP('',(#1698,#1699,#1700,#1706)); +#1698 = ORIENTED_EDGE('',*,*,#910,.F.); +#1699 = ORIENTED_EDGE('',*,*,#940,.T.); +#1700 = ORIENTED_EDGE('',*,*,#1701,.T.); +#1701 = EDGE_CURVE('',#941,#1336,#1702,.T.); +#1702 = LINE('',#1703,#1704); +#1703 = CARTESIAN_POINT('',(-375.,-608.,50.)); +#1704 = VECTOR('',#1705,1.); +#1705 = DIRECTION('',(-0.,1.,0.)); +#1706 = ORIENTED_EDGE('',*,*,#1335,.F.); +#1707 = PLANE('',#1708); +#1708 = AXIS2_PLACEMENT_3D('',#1709,#1710,#1711); +#1709 = CARTESIAN_POINT('',(-407.,-608.,50.)); +#1710 = DIRECTION('',(0.,0.,1.)); +#1711 = DIRECTION('',(1.,0.,-0.)); +#1712 = ADVANCED_FACE('',(#1713),#1719,.T.); +#1713 = FACE_BOUND('',#1714,.T.); +#1714 = EDGE_LOOP('',(#1715,#1716,#1717,#1718)); +#1715 = ORIENTED_EDGE('',*,*,#957,.F.); +#1716 = ORIENTED_EDGE('',*,*,#1701,.T.); +#1717 = ORIENTED_EDGE('',*,*,#1345,.T.); +#1718 = ORIENTED_EDGE('',*,*,#1313,.F.); +#1719 = PLANE('',#1720); +#1720 = AXIS2_PLACEMENT_3D('',#1721,#1722,#1723); +#1721 = CARTESIAN_POINT('',(-375.,-608.,50.)); +#1722 = DIRECTION('',(1.,0.,-0.)); +#1723 = DIRECTION('',(0.,0.,1.)); +#1724 = ADVANCED_FACE('',(#1725),#1741,.F.); +#1725 = FACE_BOUND('',#1726,.F.); +#1726 = EDGE_LOOP('',(#1727,#1733,#1734,#1740)); +#1727 = ORIENTED_EDGE('',*,*,#1728,.F.); +#1728 = EDGE_CURVE('',#966,#1354,#1729,.T.); +#1729 = LINE('',#1730,#1731); +#1730 = CARTESIAN_POINT('',(-400.92,-611.,550.)); +#1731 = VECTOR('',#1732,1.); +#1732 = DIRECTION('',(-0.,1.,0.)); +#1733 = ORIENTED_EDGE('',*,*,#965,.T.); +#1734 = ORIENTED_EDGE('',*,*,#1735,.T.); +#1735 = EDGE_CURVE('',#968,#1356,#1736,.T.); +#1736 = LINE('',#1737,#1738); +#1737 = CARTESIAN_POINT('',(-381.08,-611.,550.)); +#1738 = VECTOR('',#1739,1.); +#1739 = DIRECTION('',(-0.,1.,0.)); +#1740 = ORIENTED_EDGE('',*,*,#1353,.F.); +#1741 = PLANE('',#1742); +#1742 = AXIS2_PLACEMENT_3D('',#1743,#1744,#1745); +#1743 = CARTESIAN_POINT('',(-400.92,-611.,550.)); +#1744 = DIRECTION('',(0.,0.,1.)); +#1745 = DIRECTION('',(1.,0.,-0.)); +#1746 = ADVANCED_FACE('',(#1747),#1758,.T.); +#1747 = FACE_BOUND('',#1748,.T.); +#1748 = EDGE_LOOP('',(#1749,#1750,#1751,#1757)); +#1749 = ORIENTED_EDGE('',*,*,#1728,.F.); +#1750 = ORIENTED_EDGE('',*,*,#975,.T.); +#1751 = ORIENTED_EDGE('',*,*,#1752,.T.); +#1752 = EDGE_CURVE('',#976,#1364,#1753,.T.); +#1753 = LINE('',#1754,#1755); +#1754 = CARTESIAN_POINT('',(-400.92,-611.,534.)); +#1755 = VECTOR('',#1756,1.); +#1756 = DIRECTION('',(-0.,1.,0.)); +#1757 = ORIENTED_EDGE('',*,*,#1363,.F.); +#1758 = PLANE('',#1759); +#1759 = AXIS2_PLACEMENT_3D('',#1760,#1761,#1762); +#1760 = CARTESIAN_POINT('',(-400.92,-611.,534.)); +#1761 = DIRECTION('',(1.,0.,-0.)); +#1762 = DIRECTION('',(0.,0.,1.)); +#1763 = ADVANCED_FACE('',(#1764),#1775,.F.); +#1764 = FACE_BOUND('',#1765,.F.); +#1765 = EDGE_LOOP('',(#1766,#1767,#1768,#1774)); +#1766 = ORIENTED_EDGE('',*,*,#1735,.F.); +#1767 = ORIENTED_EDGE('',*,*,#991,.T.); +#1768 = ORIENTED_EDGE('',*,*,#1769,.T.); +#1769 = EDGE_CURVE('',#984,#1372,#1770,.T.); +#1770 = LINE('',#1771,#1772); +#1771 = CARTESIAN_POINT('',(-381.08,-611.,534.)); +#1772 = VECTOR('',#1773,1.); +#1773 = DIRECTION('',(-0.,1.,0.)); +#1774 = ORIENTED_EDGE('',*,*,#1379,.F.); +#1775 = PLANE('',#1776); +#1776 = AXIS2_PLACEMENT_3D('',#1777,#1778,#1779); +#1777 = CARTESIAN_POINT('',(-381.08,-611.,534.)); +#1778 = DIRECTION('',(1.,0.,-0.)); +#1779 = DIRECTION('',(0.,0.,1.)); +#1780 = ADVANCED_FACE('',(#1781),#1787,.T.); +#1781 = FACE_BOUND('',#1782,.T.); +#1782 = EDGE_LOOP('',(#1783,#1784,#1785,#1786)); +#1783 = ORIENTED_EDGE('',*,*,#1752,.F.); +#1784 = ORIENTED_EDGE('',*,*,#983,.T.); +#1785 = ORIENTED_EDGE('',*,*,#1769,.T.); +#1786 = ORIENTED_EDGE('',*,*,#1371,.F.); +#1787 = PLANE('',#1788); +#1788 = AXIS2_PLACEMENT_3D('',#1789,#1790,#1791); +#1789 = CARTESIAN_POINT('',(-400.92,-611.,534.)); +#1790 = DIRECTION('',(0.,0.,1.)); +#1791 = DIRECTION('',(1.,0.,-0.)); +#1792 = ADVANCED_FACE('',(#1793),#1809,.F.); +#1793 = FACE_BOUND('',#1794,.F.); +#1794 = EDGE_LOOP('',(#1795,#1801,#1802,#1808)); +#1795 = ORIENTED_EDGE('',*,*,#1796,.F.); +#1796 = EDGE_CURVE('',#1000,#1388,#1797,.T.); +#1797 = LINE('',#1798,#1799); +#1798 = CARTESIAN_POINT('',(-400.92,-611.,592.)); +#1799 = VECTOR('',#1800,1.); +#1800 = DIRECTION('',(-0.,1.,0.)); +#1801 = ORIENTED_EDGE('',*,*,#999,.T.); +#1802 = ORIENTED_EDGE('',*,*,#1803,.T.); +#1803 = EDGE_CURVE('',#1002,#1390,#1804,.T.); +#1804 = LINE('',#1805,#1806); +#1805 = CARTESIAN_POINT('',(-381.08,-611.,592.)); +#1806 = VECTOR('',#1807,1.); +#1807 = DIRECTION('',(-0.,1.,0.)); +#1808 = ORIENTED_EDGE('',*,*,#1387,.F.); +#1809 = PLANE('',#1810); +#1810 = AXIS2_PLACEMENT_3D('',#1811,#1812,#1813); +#1811 = CARTESIAN_POINT('',(-400.92,-611.,592.)); +#1812 = DIRECTION('',(0.,0.,1.)); +#1813 = DIRECTION('',(1.,0.,-0.)); +#1814 = ADVANCED_FACE('',(#1815),#1826,.T.); +#1815 = FACE_BOUND('',#1816,.T.); +#1816 = EDGE_LOOP('',(#1817,#1818,#1819,#1825)); +#1817 = ORIENTED_EDGE('',*,*,#1796,.F.); +#1818 = ORIENTED_EDGE('',*,*,#1009,.T.); +#1819 = ORIENTED_EDGE('',*,*,#1820,.T.); +#1820 = EDGE_CURVE('',#1010,#1398,#1821,.T.); +#1821 = LINE('',#1822,#1823); +#1822 = CARTESIAN_POINT('',(-400.92,-611.,576.)); +#1823 = VECTOR('',#1824,1.); +#1824 = DIRECTION('',(-0.,1.,0.)); +#1825 = ORIENTED_EDGE('',*,*,#1397,.F.); +#1826 = PLANE('',#1827); +#1827 = AXIS2_PLACEMENT_3D('',#1828,#1829,#1830); +#1828 = CARTESIAN_POINT('',(-400.92,-611.,576.)); +#1829 = DIRECTION('',(1.,0.,-0.)); +#1830 = DIRECTION('',(0.,0.,1.)); +#1831 = ADVANCED_FACE('',(#1832),#1843,.F.); +#1832 = FACE_BOUND('',#1833,.F.); +#1833 = EDGE_LOOP('',(#1834,#1835,#1836,#1842)); +#1834 = ORIENTED_EDGE('',*,*,#1803,.F.); +#1835 = ORIENTED_EDGE('',*,*,#1025,.T.); +#1836 = ORIENTED_EDGE('',*,*,#1837,.T.); +#1837 = EDGE_CURVE('',#1018,#1406,#1838,.T.); +#1838 = LINE('',#1839,#1840); +#1839 = CARTESIAN_POINT('',(-381.08,-611.,576.)); +#1840 = VECTOR('',#1841,1.); +#1841 = DIRECTION('',(-0.,1.,0.)); +#1842 = ORIENTED_EDGE('',*,*,#1413,.F.); +#1843 = PLANE('',#1844); +#1844 = AXIS2_PLACEMENT_3D('',#1845,#1846,#1847); +#1845 = CARTESIAN_POINT('',(-381.08,-611.,576.)); +#1846 = DIRECTION('',(1.,0.,-0.)); +#1847 = DIRECTION('',(0.,0.,1.)); +#1848 = ADVANCED_FACE('',(#1849),#1855,.T.); +#1849 = FACE_BOUND('',#1850,.T.); +#1850 = EDGE_LOOP('',(#1851,#1852,#1853,#1854)); +#1851 = ORIENTED_EDGE('',*,*,#1820,.F.); +#1852 = ORIENTED_EDGE('',*,*,#1017,.T.); +#1853 = ORIENTED_EDGE('',*,*,#1837,.T.); +#1854 = ORIENTED_EDGE('',*,*,#1405,.F.); +#1855 = PLANE('',#1856); +#1856 = AXIS2_PLACEMENT_3D('',#1857,#1858,#1859); +#1857 = CARTESIAN_POINT('',(-400.92,-611.,576.)); +#1858 = DIRECTION('',(0.,0.,1.)); +#1859 = DIRECTION('',(1.,0.,-0.)); +#1860 = ADVANCED_FACE('',(#1861),#1877,.F.); +#1861 = FACE_BOUND('',#1862,.F.); +#1862 = EDGE_LOOP('',(#1863,#1869,#1870,#1876)); +#1863 = ORIENTED_EDGE('',*,*,#1864,.F.); +#1864 = EDGE_CURVE('',#1034,#1422,#1865,.T.); +#1865 = LINE('',#1866,#1867); +#1866 = CARTESIAN_POINT('',(-400.92,-611.,634.)); +#1867 = VECTOR('',#1868,1.); +#1868 = DIRECTION('',(-0.,1.,0.)); +#1869 = ORIENTED_EDGE('',*,*,#1033,.T.); +#1870 = ORIENTED_EDGE('',*,*,#1871,.T.); +#1871 = EDGE_CURVE('',#1036,#1424,#1872,.T.); +#1872 = LINE('',#1873,#1874); +#1873 = CARTESIAN_POINT('',(-381.08,-611.,634.)); +#1874 = VECTOR('',#1875,1.); +#1875 = DIRECTION('',(-0.,1.,0.)); +#1876 = ORIENTED_EDGE('',*,*,#1421,.F.); +#1877 = PLANE('',#1878); +#1878 = AXIS2_PLACEMENT_3D('',#1879,#1880,#1881); +#1879 = CARTESIAN_POINT('',(-400.92,-611.,634.)); +#1880 = DIRECTION('',(0.,0.,1.)); +#1881 = DIRECTION('',(1.,0.,-0.)); +#1882 = ADVANCED_FACE('',(#1883),#1894,.T.); +#1883 = FACE_BOUND('',#1884,.T.); +#1884 = EDGE_LOOP('',(#1885,#1886,#1887,#1893)); +#1885 = ORIENTED_EDGE('',*,*,#1864,.F.); +#1886 = ORIENTED_EDGE('',*,*,#1043,.T.); +#1887 = ORIENTED_EDGE('',*,*,#1888,.T.); +#1888 = EDGE_CURVE('',#1044,#1432,#1889,.T.); +#1889 = LINE('',#1890,#1891); +#1890 = CARTESIAN_POINT('',(-400.92,-611.,618.)); +#1891 = VECTOR('',#1892,1.); +#1892 = DIRECTION('',(-0.,1.,0.)); +#1893 = ORIENTED_EDGE('',*,*,#1431,.F.); +#1894 = PLANE('',#1895); +#1895 = AXIS2_PLACEMENT_3D('',#1896,#1897,#1898); +#1896 = CARTESIAN_POINT('',(-400.92,-611.,618.)); +#1897 = DIRECTION('',(1.,0.,-0.)); +#1898 = DIRECTION('',(0.,0.,1.)); +#1899 = ADVANCED_FACE('',(#1900),#1911,.F.); +#1900 = FACE_BOUND('',#1901,.F.); +#1901 = EDGE_LOOP('',(#1902,#1903,#1904,#1910)); +#1902 = ORIENTED_EDGE('',*,*,#1871,.F.); +#1903 = ORIENTED_EDGE('',*,*,#1059,.T.); +#1904 = ORIENTED_EDGE('',*,*,#1905,.T.); +#1905 = EDGE_CURVE('',#1052,#1440,#1906,.T.); +#1906 = LINE('',#1907,#1908); +#1907 = CARTESIAN_POINT('',(-381.08,-611.,618.)); +#1908 = VECTOR('',#1909,1.); +#1909 = DIRECTION('',(-0.,1.,0.)); +#1910 = ORIENTED_EDGE('',*,*,#1447,.F.); +#1911 = PLANE('',#1912); +#1912 = AXIS2_PLACEMENT_3D('',#1913,#1914,#1915); +#1913 = CARTESIAN_POINT('',(-381.08,-611.,618.)); +#1914 = DIRECTION('',(1.,0.,-0.)); +#1915 = DIRECTION('',(0.,0.,1.)); +#1916 = ADVANCED_FACE('',(#1917),#1923,.T.); +#1917 = FACE_BOUND('',#1918,.T.); +#1918 = EDGE_LOOP('',(#1919,#1920,#1921,#1922)); +#1919 = ORIENTED_EDGE('',*,*,#1888,.F.); +#1920 = ORIENTED_EDGE('',*,*,#1051,.T.); +#1921 = ORIENTED_EDGE('',*,*,#1905,.T.); +#1922 = ORIENTED_EDGE('',*,*,#1439,.F.); +#1923 = PLANE('',#1924); +#1924 = AXIS2_PLACEMENT_3D('',#1925,#1926,#1927); +#1925 = CARTESIAN_POINT('',(-400.92,-611.,618.)); +#1926 = DIRECTION('',(0.,0.,1.)); +#1927 = DIRECTION('',(1.,0.,-0.)); +#1928 = ADVANCED_FACE('',(#1929),#1945,.F.); +#1929 = FACE_BOUND('',#1930,.F.); +#1930 = EDGE_LOOP('',(#1931,#1937,#1938,#1944)); +#1931 = ORIENTED_EDGE('',*,*,#1932,.F.); +#1932 = EDGE_CURVE('',#1068,#1456,#1933,.T.); +#1933 = LINE('',#1934,#1935); +#1934 = CARTESIAN_POINT('',(-400.92,-611.,676.)); +#1935 = VECTOR('',#1936,1.); +#1936 = DIRECTION('',(-0.,1.,0.)); +#1937 = ORIENTED_EDGE('',*,*,#1067,.T.); +#1938 = ORIENTED_EDGE('',*,*,#1939,.T.); +#1939 = EDGE_CURVE('',#1070,#1458,#1940,.T.); +#1940 = LINE('',#1941,#1942); +#1941 = CARTESIAN_POINT('',(-381.08,-611.,676.)); +#1942 = VECTOR('',#1943,1.); +#1943 = DIRECTION('',(-0.,1.,0.)); +#1944 = ORIENTED_EDGE('',*,*,#1455,.F.); +#1945 = PLANE('',#1946); +#1946 = AXIS2_PLACEMENT_3D('',#1947,#1948,#1949); +#1947 = CARTESIAN_POINT('',(-400.92,-611.,676.)); +#1948 = DIRECTION('',(0.,0.,1.)); +#1949 = DIRECTION('',(1.,0.,-0.)); +#1950 = ADVANCED_FACE('',(#1951),#1962,.T.); +#1951 = FACE_BOUND('',#1952,.T.); +#1952 = EDGE_LOOP('',(#1953,#1954,#1955,#1961)); +#1953 = ORIENTED_EDGE('',*,*,#1932,.F.); +#1954 = ORIENTED_EDGE('',*,*,#1077,.T.); +#1955 = ORIENTED_EDGE('',*,*,#1956,.T.); +#1956 = EDGE_CURVE('',#1078,#1466,#1957,.T.); +#1957 = LINE('',#1958,#1959); +#1958 = CARTESIAN_POINT('',(-400.92,-611.,660.)); +#1959 = VECTOR('',#1960,1.); +#1960 = DIRECTION('',(-0.,1.,0.)); +#1961 = ORIENTED_EDGE('',*,*,#1465,.F.); +#1962 = PLANE('',#1963); +#1963 = AXIS2_PLACEMENT_3D('',#1964,#1965,#1966); +#1964 = CARTESIAN_POINT('',(-400.92,-611.,660.)); +#1965 = DIRECTION('',(1.,0.,-0.)); +#1966 = DIRECTION('',(0.,0.,1.)); +#1967 = ADVANCED_FACE('',(#1968),#1979,.F.); +#1968 = FACE_BOUND('',#1969,.F.); +#1969 = EDGE_LOOP('',(#1970,#1971,#1972,#1978)); +#1970 = ORIENTED_EDGE('',*,*,#1939,.F.); +#1971 = ORIENTED_EDGE('',*,*,#1093,.T.); +#1972 = ORIENTED_EDGE('',*,*,#1973,.T.); +#1973 = EDGE_CURVE('',#1086,#1474,#1974,.T.); +#1974 = LINE('',#1975,#1976); +#1975 = CARTESIAN_POINT('',(-381.08,-611.,660.)); +#1976 = VECTOR('',#1977,1.); +#1977 = DIRECTION('',(-0.,1.,0.)); +#1978 = ORIENTED_EDGE('',*,*,#1481,.F.); +#1979 = PLANE('',#1980); +#1980 = AXIS2_PLACEMENT_3D('',#1981,#1982,#1983); +#1981 = CARTESIAN_POINT('',(-381.08,-611.,660.)); +#1982 = DIRECTION('',(1.,0.,-0.)); +#1983 = DIRECTION('',(0.,0.,1.)); +#1984 = ADVANCED_FACE('',(#1985),#1991,.T.); +#1985 = FACE_BOUND('',#1986,.T.); +#1986 = EDGE_LOOP('',(#1987,#1988,#1989,#1990)); +#1987 = ORIENTED_EDGE('',*,*,#1956,.F.); +#1988 = ORIENTED_EDGE('',*,*,#1085,.T.); +#1989 = ORIENTED_EDGE('',*,*,#1973,.T.); +#1990 = ORIENTED_EDGE('',*,*,#1473,.F.); +#1991 = PLANE('',#1992); +#1992 = AXIS2_PLACEMENT_3D('',#1993,#1994,#1995); +#1993 = CARTESIAN_POINT('',(-400.92,-611.,660.)); +#1994 = DIRECTION('',(0.,0.,1.)); +#1995 = DIRECTION('',(1.,0.,-0.)); +#1996 = ADVANCED_FACE('',(#1997),#2013,.F.); +#1997 = FACE_BOUND('',#1998,.F.); +#1998 = EDGE_LOOP('',(#1999,#2005,#2006,#2012)); +#1999 = ORIENTED_EDGE('',*,*,#2000,.F.); +#2000 = EDGE_CURVE('',#1102,#1490,#2001,.T.); +#2001 = LINE('',#2002,#2003); +#2002 = CARTESIAN_POINT('',(-400.92,-611.,718.)); +#2003 = VECTOR('',#2004,1.); +#2004 = DIRECTION('',(-0.,1.,0.)); +#2005 = ORIENTED_EDGE('',*,*,#1101,.T.); +#2006 = ORIENTED_EDGE('',*,*,#2007,.T.); +#2007 = EDGE_CURVE('',#1104,#1492,#2008,.T.); +#2008 = LINE('',#2009,#2010); +#2009 = CARTESIAN_POINT('',(-381.08,-611.,718.)); +#2010 = VECTOR('',#2011,1.); +#2011 = DIRECTION('',(-0.,1.,0.)); +#2012 = ORIENTED_EDGE('',*,*,#1489,.F.); +#2013 = PLANE('',#2014); +#2014 = AXIS2_PLACEMENT_3D('',#2015,#2016,#2017); +#2015 = CARTESIAN_POINT('',(-400.92,-611.,718.)); +#2016 = DIRECTION('',(0.,0.,1.)); +#2017 = DIRECTION('',(1.,0.,-0.)); +#2018 = ADVANCED_FACE('',(#2019),#2030,.T.); +#2019 = FACE_BOUND('',#2020,.T.); +#2020 = EDGE_LOOP('',(#2021,#2022,#2023,#2029)); +#2021 = ORIENTED_EDGE('',*,*,#2000,.F.); +#2022 = ORIENTED_EDGE('',*,*,#1111,.T.); +#2023 = ORIENTED_EDGE('',*,*,#2024,.T.); +#2024 = EDGE_CURVE('',#1112,#1500,#2025,.T.); +#2025 = LINE('',#2026,#2027); +#2026 = CARTESIAN_POINT('',(-400.92,-611.,702.)); +#2027 = VECTOR('',#2028,1.); +#2028 = DIRECTION('',(-0.,1.,0.)); +#2029 = ORIENTED_EDGE('',*,*,#1499,.F.); +#2030 = PLANE('',#2031); +#2031 = AXIS2_PLACEMENT_3D('',#2032,#2033,#2034); +#2032 = CARTESIAN_POINT('',(-400.92,-611.,702.)); +#2033 = DIRECTION('',(1.,0.,-0.)); +#2034 = DIRECTION('',(0.,0.,1.)); +#2035 = ADVANCED_FACE('',(#2036),#2047,.F.); +#2036 = FACE_BOUND('',#2037,.F.); +#2037 = EDGE_LOOP('',(#2038,#2039,#2040,#2046)); +#2038 = ORIENTED_EDGE('',*,*,#2007,.F.); +#2039 = ORIENTED_EDGE('',*,*,#1127,.T.); +#2040 = ORIENTED_EDGE('',*,*,#2041,.T.); +#2041 = EDGE_CURVE('',#1120,#1508,#2042,.T.); +#2042 = LINE('',#2043,#2044); +#2043 = CARTESIAN_POINT('',(-381.08,-611.,702.)); +#2044 = VECTOR('',#2045,1.); +#2045 = DIRECTION('',(-0.,1.,0.)); +#2046 = ORIENTED_EDGE('',*,*,#1515,.F.); +#2047 = PLANE('',#2048); +#2048 = AXIS2_PLACEMENT_3D('',#2049,#2050,#2051); +#2049 = CARTESIAN_POINT('',(-381.08,-611.,702.)); +#2050 = DIRECTION('',(1.,0.,-0.)); +#2051 = DIRECTION('',(0.,0.,1.)); +#2052 = ADVANCED_FACE('',(#2053),#2059,.T.); +#2053 = FACE_BOUND('',#2054,.T.); +#2054 = EDGE_LOOP('',(#2055,#2056,#2057,#2058)); +#2055 = ORIENTED_EDGE('',*,*,#2024,.F.); +#2056 = ORIENTED_EDGE('',*,*,#1119,.T.); +#2057 = ORIENTED_EDGE('',*,*,#2041,.T.); +#2058 = ORIENTED_EDGE('',*,*,#1507,.F.); +#2059 = PLANE('',#2060); +#2060 = AXIS2_PLACEMENT_3D('',#2061,#2062,#2063); +#2061 = CARTESIAN_POINT('',(-400.92,-611.,702.)); +#2062 = DIRECTION('',(0.,0.,1.)); +#2063 = DIRECTION('',(1.,0.,-0.)); +#2064 = ADVANCED_FACE('',(#2065),#2081,.F.); +#2065 = FACE_BOUND('',#2066,.F.); +#2066 = EDGE_LOOP('',(#2067,#2073,#2074,#2080)); +#2067 = ORIENTED_EDGE('',*,*,#2068,.F.); +#2068 = EDGE_CURVE('',#1136,#1524,#2069,.T.); +#2069 = LINE('',#2070,#2071); +#2070 = CARTESIAN_POINT('',(-400.92,-611.,760.)); +#2071 = VECTOR('',#2072,1.); +#2072 = DIRECTION('',(-0.,1.,0.)); +#2073 = ORIENTED_EDGE('',*,*,#1135,.T.); +#2074 = ORIENTED_EDGE('',*,*,#2075,.T.); +#2075 = EDGE_CURVE('',#1138,#1526,#2076,.T.); +#2076 = LINE('',#2077,#2078); +#2077 = CARTESIAN_POINT('',(-381.08,-611.,760.)); +#2078 = VECTOR('',#2079,1.); +#2079 = DIRECTION('',(-0.,1.,0.)); +#2080 = ORIENTED_EDGE('',*,*,#1523,.F.); +#2081 = PLANE('',#2082); +#2082 = AXIS2_PLACEMENT_3D('',#2083,#2084,#2085); +#2083 = CARTESIAN_POINT('',(-400.92,-611.,760.)); +#2084 = DIRECTION('',(0.,0.,1.)); +#2085 = DIRECTION('',(1.,0.,-0.)); +#2086 = ADVANCED_FACE('',(#2087),#2098,.T.); +#2087 = FACE_BOUND('',#2088,.T.); +#2088 = EDGE_LOOP('',(#2089,#2090,#2091,#2097)); +#2089 = ORIENTED_EDGE('',*,*,#2068,.F.); +#2090 = ORIENTED_EDGE('',*,*,#1145,.T.); +#2091 = ORIENTED_EDGE('',*,*,#2092,.T.); +#2092 = EDGE_CURVE('',#1146,#1534,#2093,.T.); +#2093 = LINE('',#2094,#2095); +#2094 = CARTESIAN_POINT('',(-400.92,-611.,744.)); +#2095 = VECTOR('',#2096,1.); +#2096 = DIRECTION('',(-0.,1.,0.)); +#2097 = ORIENTED_EDGE('',*,*,#1533,.F.); +#2098 = PLANE('',#2099); +#2099 = AXIS2_PLACEMENT_3D('',#2100,#2101,#2102); +#2100 = CARTESIAN_POINT('',(-400.92,-611.,744.)); +#2101 = DIRECTION('',(1.,0.,-0.)); +#2102 = DIRECTION('',(0.,0.,1.)); +#2103 = ADVANCED_FACE('',(#2104),#2115,.F.); +#2104 = FACE_BOUND('',#2105,.F.); +#2105 = EDGE_LOOP('',(#2106,#2107,#2108,#2114)); +#2106 = ORIENTED_EDGE('',*,*,#2075,.F.); +#2107 = ORIENTED_EDGE('',*,*,#1161,.T.); +#2108 = ORIENTED_EDGE('',*,*,#2109,.T.); +#2109 = EDGE_CURVE('',#1154,#1542,#2110,.T.); +#2110 = LINE('',#2111,#2112); +#2111 = CARTESIAN_POINT('',(-381.08,-611.,744.)); +#2112 = VECTOR('',#2113,1.); +#2113 = DIRECTION('',(-0.,1.,0.)); +#2114 = ORIENTED_EDGE('',*,*,#1549,.F.); +#2115 = PLANE('',#2116); +#2116 = AXIS2_PLACEMENT_3D('',#2117,#2118,#2119); +#2117 = CARTESIAN_POINT('',(-381.08,-611.,744.)); +#2118 = DIRECTION('',(1.,0.,-0.)); +#2119 = DIRECTION('',(0.,0.,1.)); +#2120 = ADVANCED_FACE('',(#2121),#2127,.T.); +#2121 = FACE_BOUND('',#2122,.T.); +#2122 = EDGE_LOOP('',(#2123,#2124,#2125,#2126)); +#2123 = ORIENTED_EDGE('',*,*,#2092,.F.); +#2124 = ORIENTED_EDGE('',*,*,#1153,.T.); +#2125 = ORIENTED_EDGE('',*,*,#2109,.T.); +#2126 = ORIENTED_EDGE('',*,*,#1541,.F.); +#2127 = PLANE('',#2128); +#2128 = AXIS2_PLACEMENT_3D('',#2129,#2130,#2131); +#2129 = CARTESIAN_POINT('',(-400.92,-611.,744.)); +#2130 = DIRECTION('',(0.,0.,1.)); +#2131 = DIRECTION('',(1.,0.,-0.)); +#2132 = ADVANCED_FACE('',(#2133),#2149,.F.); +#2133 = FACE_BOUND('',#2134,.F.); +#2134 = EDGE_LOOP('',(#2135,#2141,#2142,#2148)); +#2135 = ORIENTED_EDGE('',*,*,#2136,.F.); +#2136 = EDGE_CURVE('',#1170,#1558,#2137,.T.); +#2137 = LINE('',#2138,#2139); +#2138 = CARTESIAN_POINT('',(-400.92,-611.,802.)); +#2139 = VECTOR('',#2140,1.); +#2140 = DIRECTION('',(-0.,1.,0.)); +#2141 = ORIENTED_EDGE('',*,*,#1169,.T.); +#2142 = ORIENTED_EDGE('',*,*,#2143,.T.); +#2143 = EDGE_CURVE('',#1172,#1560,#2144,.T.); +#2144 = LINE('',#2145,#2146); +#2145 = CARTESIAN_POINT('',(-381.08,-611.,802.)); +#2146 = VECTOR('',#2147,1.); +#2147 = DIRECTION('',(-0.,1.,0.)); +#2148 = ORIENTED_EDGE('',*,*,#1557,.F.); +#2149 = PLANE('',#2150); +#2150 = AXIS2_PLACEMENT_3D('',#2151,#2152,#2153); +#2151 = CARTESIAN_POINT('',(-400.92,-611.,802.)); +#2152 = DIRECTION('',(0.,0.,1.)); +#2153 = DIRECTION('',(1.,0.,-0.)); +#2154 = ADVANCED_FACE('',(#2155),#2166,.T.); +#2155 = FACE_BOUND('',#2156,.T.); +#2156 = EDGE_LOOP('',(#2157,#2158,#2159,#2165)); +#2157 = ORIENTED_EDGE('',*,*,#2136,.F.); +#2158 = ORIENTED_EDGE('',*,*,#1179,.T.); +#2159 = ORIENTED_EDGE('',*,*,#2160,.T.); +#2160 = EDGE_CURVE('',#1180,#1568,#2161,.T.); +#2161 = LINE('',#2162,#2163); +#2162 = CARTESIAN_POINT('',(-400.92,-611.,786.)); +#2163 = VECTOR('',#2164,1.); +#2164 = DIRECTION('',(-0.,1.,0.)); +#2165 = ORIENTED_EDGE('',*,*,#1567,.F.); +#2166 = PLANE('',#2167); +#2167 = AXIS2_PLACEMENT_3D('',#2168,#2169,#2170); +#2168 = CARTESIAN_POINT('',(-400.92,-611.,786.)); +#2169 = DIRECTION('',(1.,0.,-0.)); +#2170 = DIRECTION('',(0.,0.,1.)); +#2171 = ADVANCED_FACE('',(#2172),#2183,.F.); +#2172 = FACE_BOUND('',#2173,.F.); +#2173 = EDGE_LOOP('',(#2174,#2175,#2176,#2182)); +#2174 = ORIENTED_EDGE('',*,*,#2143,.F.); +#2175 = ORIENTED_EDGE('',*,*,#1195,.T.); +#2176 = ORIENTED_EDGE('',*,*,#2177,.T.); +#2177 = EDGE_CURVE('',#1188,#1576,#2178,.T.); +#2178 = LINE('',#2179,#2180); +#2179 = CARTESIAN_POINT('',(-381.08,-611.,786.)); +#2180 = VECTOR('',#2181,1.); +#2181 = DIRECTION('',(-0.,1.,0.)); +#2182 = ORIENTED_EDGE('',*,*,#1583,.F.); +#2183 = PLANE('',#2184); +#2184 = AXIS2_PLACEMENT_3D('',#2185,#2186,#2187); +#2185 = CARTESIAN_POINT('',(-381.08,-611.,786.)); +#2186 = DIRECTION('',(1.,0.,-0.)); +#2187 = DIRECTION('',(0.,0.,1.)); +#2188 = ADVANCED_FACE('',(#2189),#2195,.T.); +#2189 = FACE_BOUND('',#2190,.T.); +#2190 = EDGE_LOOP('',(#2191,#2192,#2193,#2194)); +#2191 = ORIENTED_EDGE('',*,*,#2160,.F.); +#2192 = ORIENTED_EDGE('',*,*,#1187,.T.); +#2193 = ORIENTED_EDGE('',*,*,#2177,.T.); +#2194 = ORIENTED_EDGE('',*,*,#1575,.F.); +#2195 = PLANE('',#2196); +#2196 = AXIS2_PLACEMENT_3D('',#2197,#2198,#2199); +#2197 = CARTESIAN_POINT('',(-400.92,-611.,786.)); +#2198 = DIRECTION('',(0.,0.,1.)); +#2199 = DIRECTION('',(1.,0.,-0.)); +#2200 = ADVANCED_FACE('',(#2201),#2217,.F.); +#2201 = FACE_BOUND('',#2202,.F.); +#2202 = EDGE_LOOP('',(#2203,#2209,#2210,#2216)); +#2203 = ORIENTED_EDGE('',*,*,#2204,.F.); +#2204 = EDGE_CURVE('',#1204,#1592,#2205,.T.); +#2205 = LINE('',#2206,#2207); +#2206 = CARTESIAN_POINT('',(-400.92,-611.,844.)); +#2207 = VECTOR('',#2208,1.); +#2208 = DIRECTION('',(-0.,1.,0.)); +#2209 = ORIENTED_EDGE('',*,*,#1203,.T.); +#2210 = ORIENTED_EDGE('',*,*,#2211,.T.); +#2211 = EDGE_CURVE('',#1206,#1594,#2212,.T.); +#2212 = LINE('',#2213,#2214); +#2213 = CARTESIAN_POINT('',(-381.08,-611.,844.)); +#2214 = VECTOR('',#2215,1.); +#2215 = DIRECTION('',(-0.,1.,0.)); +#2216 = ORIENTED_EDGE('',*,*,#1591,.F.); +#2217 = PLANE('',#2218); +#2218 = AXIS2_PLACEMENT_3D('',#2219,#2220,#2221); +#2219 = CARTESIAN_POINT('',(-400.92,-611.,844.)); +#2220 = DIRECTION('',(0.,0.,1.)); +#2221 = DIRECTION('',(1.,0.,-0.)); +#2222 = ADVANCED_FACE('',(#2223),#2234,.T.); +#2223 = FACE_BOUND('',#2224,.T.); +#2224 = EDGE_LOOP('',(#2225,#2226,#2227,#2233)); +#2225 = ORIENTED_EDGE('',*,*,#2204,.F.); +#2226 = ORIENTED_EDGE('',*,*,#1213,.T.); +#2227 = ORIENTED_EDGE('',*,*,#2228,.T.); +#2228 = EDGE_CURVE('',#1214,#1602,#2229,.T.); +#2229 = LINE('',#2230,#2231); +#2230 = CARTESIAN_POINT('',(-400.92,-611.,828.)); +#2231 = VECTOR('',#2232,1.); +#2232 = DIRECTION('',(-0.,1.,0.)); +#2233 = ORIENTED_EDGE('',*,*,#1601,.F.); +#2234 = PLANE('',#2235); +#2235 = AXIS2_PLACEMENT_3D('',#2236,#2237,#2238); +#2236 = CARTESIAN_POINT('',(-400.92,-611.,828.)); +#2237 = DIRECTION('',(1.,0.,-0.)); +#2238 = DIRECTION('',(0.,0.,1.)); +#2239 = ADVANCED_FACE('',(#2240),#2251,.F.); +#2240 = FACE_BOUND('',#2241,.F.); +#2241 = EDGE_LOOP('',(#2242,#2243,#2244,#2250)); +#2242 = ORIENTED_EDGE('',*,*,#2211,.F.); +#2243 = ORIENTED_EDGE('',*,*,#1229,.T.); +#2244 = ORIENTED_EDGE('',*,*,#2245,.T.); +#2245 = EDGE_CURVE('',#1222,#1610,#2246,.T.); +#2246 = LINE('',#2247,#2248); +#2247 = CARTESIAN_POINT('',(-381.08,-611.,828.)); +#2248 = VECTOR('',#2249,1.); +#2249 = DIRECTION('',(-0.,1.,0.)); +#2250 = ORIENTED_EDGE('',*,*,#1617,.F.); +#2251 = PLANE('',#2252); +#2252 = AXIS2_PLACEMENT_3D('',#2253,#2254,#2255); +#2253 = CARTESIAN_POINT('',(-381.08,-611.,828.)); +#2254 = DIRECTION('',(1.,0.,-0.)); +#2255 = DIRECTION('',(0.,0.,1.)); +#2256 = ADVANCED_FACE('',(#2257),#2263,.T.); +#2257 = FACE_BOUND('',#2258,.T.); +#2258 = EDGE_LOOP('',(#2259,#2260,#2261,#2262)); +#2259 = ORIENTED_EDGE('',*,*,#2228,.F.); +#2260 = ORIENTED_EDGE('',*,*,#1221,.T.); +#2261 = ORIENTED_EDGE('',*,*,#2245,.T.); +#2262 = ORIENTED_EDGE('',*,*,#1609,.F.); +#2263 = PLANE('',#2264); +#2264 = AXIS2_PLACEMENT_3D('',#2265,#2266,#2267); +#2265 = CARTESIAN_POINT('',(-400.92,-611.,828.)); +#2266 = DIRECTION('',(0.,0.,1.)); +#2267 = DIRECTION('',(1.,0.,-0.)); +#2268 = ADVANCED_FACE('',(#2269),#2285,.F.); +#2269 = FACE_BOUND('',#2270,.F.); +#2270 = EDGE_LOOP('',(#2271,#2277,#2278,#2284)); +#2271 = ORIENTED_EDGE('',*,*,#2272,.F.); +#2272 = EDGE_CURVE('',#1238,#1626,#2273,.T.); +#2273 = LINE('',#2274,#2275); +#2274 = CARTESIAN_POINT('',(-400.92,-611.,886.)); +#2275 = VECTOR('',#2276,1.); +#2276 = DIRECTION('',(-0.,1.,0.)); +#2277 = ORIENTED_EDGE('',*,*,#1237,.T.); +#2278 = ORIENTED_EDGE('',*,*,#2279,.T.); +#2279 = EDGE_CURVE('',#1240,#1628,#2280,.T.); +#2280 = LINE('',#2281,#2282); +#2281 = CARTESIAN_POINT('',(-381.08,-611.,886.)); +#2282 = VECTOR('',#2283,1.); +#2283 = DIRECTION('',(-0.,1.,0.)); +#2284 = ORIENTED_EDGE('',*,*,#1625,.F.); +#2285 = PLANE('',#2286); +#2286 = AXIS2_PLACEMENT_3D('',#2287,#2288,#2289); +#2287 = CARTESIAN_POINT('',(-400.92,-611.,886.)); +#2288 = DIRECTION('',(0.,0.,1.)); +#2289 = DIRECTION('',(1.,0.,-0.)); +#2290 = ADVANCED_FACE('',(#2291),#2302,.T.); +#2291 = FACE_BOUND('',#2292,.T.); +#2292 = EDGE_LOOP('',(#2293,#2294,#2295,#2301)); +#2293 = ORIENTED_EDGE('',*,*,#2272,.F.); +#2294 = ORIENTED_EDGE('',*,*,#1247,.T.); +#2295 = ORIENTED_EDGE('',*,*,#2296,.T.); +#2296 = EDGE_CURVE('',#1248,#1636,#2297,.T.); +#2297 = LINE('',#2298,#2299); +#2298 = CARTESIAN_POINT('',(-400.92,-611.,870.)); +#2299 = VECTOR('',#2300,1.); +#2300 = DIRECTION('',(-0.,1.,0.)); +#2301 = ORIENTED_EDGE('',*,*,#1635,.F.); +#2302 = PLANE('',#2303); +#2303 = AXIS2_PLACEMENT_3D('',#2304,#2305,#2306); +#2304 = CARTESIAN_POINT('',(-400.92,-611.,870.)); +#2305 = DIRECTION('',(1.,0.,-0.)); +#2306 = DIRECTION('',(0.,0.,1.)); +#2307 = ADVANCED_FACE('',(#2308),#2319,.F.); +#2308 = FACE_BOUND('',#2309,.F.); +#2309 = EDGE_LOOP('',(#2310,#2311,#2312,#2318)); +#2310 = ORIENTED_EDGE('',*,*,#2279,.F.); +#2311 = ORIENTED_EDGE('',*,*,#1263,.T.); +#2312 = ORIENTED_EDGE('',*,*,#2313,.T.); +#2313 = EDGE_CURVE('',#1256,#1644,#2314,.T.); +#2314 = LINE('',#2315,#2316); +#2315 = CARTESIAN_POINT('',(-381.08,-611.,870.)); +#2316 = VECTOR('',#2317,1.); +#2317 = DIRECTION('',(-0.,1.,0.)); +#2318 = ORIENTED_EDGE('',*,*,#1651,.F.); +#2319 = PLANE('',#2320); +#2320 = AXIS2_PLACEMENT_3D('',#2321,#2322,#2323); +#2321 = CARTESIAN_POINT('',(-381.08,-611.,870.)); +#2322 = DIRECTION('',(1.,0.,-0.)); +#2323 = DIRECTION('',(0.,0.,1.)); +#2324 = ADVANCED_FACE('',(#2325),#2331,.T.); +#2325 = FACE_BOUND('',#2326,.T.); +#2326 = EDGE_LOOP('',(#2327,#2328,#2329,#2330)); +#2327 = ORIENTED_EDGE('',*,*,#2296,.F.); +#2328 = ORIENTED_EDGE('',*,*,#1255,.T.); +#2329 = ORIENTED_EDGE('',*,*,#2313,.T.); +#2330 = ORIENTED_EDGE('',*,*,#1643,.F.); +#2331 = PLANE('',#2332); +#2332 = AXIS2_PLACEMENT_3D('',#2333,#2334,#2335); +#2333 = CARTESIAN_POINT('',(-400.92,-611.,870.)); +#2334 = DIRECTION('',(0.,0.,1.)); +#2335 = DIRECTION('',(1.,0.,-0.)); +#2336 = ADVANCED_FACE('',(#2337),#2353,.F.); +#2337 = FACE_BOUND('',#2338,.F.); +#2338 = EDGE_LOOP('',(#2339,#2345,#2346,#2352)); +#2339 = ORIENTED_EDGE('',*,*,#2340,.F.); +#2340 = EDGE_CURVE('',#1272,#1660,#2341,.T.); +#2341 = LINE('',#2342,#2343); +#2342 = CARTESIAN_POINT('',(-400.92,-611.,928.)); +#2343 = VECTOR('',#2344,1.); +#2344 = DIRECTION('',(-0.,1.,0.)); +#2345 = ORIENTED_EDGE('',*,*,#1271,.T.); +#2346 = ORIENTED_EDGE('',*,*,#2347,.T.); +#2347 = EDGE_CURVE('',#1274,#1662,#2348,.T.); +#2348 = LINE('',#2349,#2350); +#2349 = CARTESIAN_POINT('',(-381.08,-611.,928.)); +#2350 = VECTOR('',#2351,1.); +#2351 = DIRECTION('',(-0.,1.,0.)); +#2352 = ORIENTED_EDGE('',*,*,#1659,.F.); +#2353 = PLANE('',#2354); +#2354 = AXIS2_PLACEMENT_3D('',#2355,#2356,#2357); +#2355 = CARTESIAN_POINT('',(-400.92,-611.,928.)); +#2356 = DIRECTION('',(0.,0.,1.)); +#2357 = DIRECTION('',(1.,0.,-0.)); +#2358 = ADVANCED_FACE('',(#2359),#2370,.F.); +#2359 = FACE_BOUND('',#2360,.F.); +#2360 = EDGE_LOOP('',(#2361,#2362,#2363,#2369)); +#2361 = ORIENTED_EDGE('',*,*,#2347,.F.); +#2362 = ORIENTED_EDGE('',*,*,#1297,.T.); +#2363 = ORIENTED_EDGE('',*,*,#2364,.T.); +#2364 = EDGE_CURVE('',#1290,#1678,#2365,.T.); +#2365 = LINE('',#2366,#2367); +#2366 = CARTESIAN_POINT('',(-381.08,-611.,912.)); +#2367 = VECTOR('',#2368,1.); +#2368 = DIRECTION('',(-0.,1.,0.)); +#2369 = ORIENTED_EDGE('',*,*,#1685,.F.); +#2370 = PLANE('',#2371); +#2371 = AXIS2_PLACEMENT_3D('',#2372,#2373,#2374); +#2372 = CARTESIAN_POINT('',(-381.08,-611.,912.)); +#2373 = DIRECTION('',(1.,0.,-0.)); +#2374 = DIRECTION('',(0.,0.,1.)); +#2375 = ADVANCED_FACE('',(#2376),#2387,.T.); +#2376 = FACE_BOUND('',#2377,.T.); +#2377 = EDGE_LOOP('',(#2378,#2384,#2385,#2386)); +#2378 = ORIENTED_EDGE('',*,*,#2379,.F.); +#2379 = EDGE_CURVE('',#1282,#1670,#2380,.T.); +#2380 = LINE('',#2381,#2382); +#2381 = CARTESIAN_POINT('',(-400.92,-611.,912.)); +#2382 = VECTOR('',#2383,1.); +#2383 = DIRECTION('',(-0.,1.,0.)); +#2384 = ORIENTED_EDGE('',*,*,#1289,.T.); +#2385 = ORIENTED_EDGE('',*,*,#2364,.T.); +#2386 = ORIENTED_EDGE('',*,*,#1677,.F.); +#2387 = PLANE('',#2388); +#2388 = AXIS2_PLACEMENT_3D('',#2389,#2390,#2391); +#2389 = CARTESIAN_POINT('',(-400.92,-611.,912.)); +#2390 = DIRECTION('',(0.,0.,1.)); +#2391 = DIRECTION('',(1.,0.,-0.)); +#2392 = ADVANCED_FACE('',(#2393),#2399,.T.); +#2393 = FACE_BOUND('',#2394,.T.); +#2394 = EDGE_LOOP('',(#2395,#2396,#2397,#2398)); +#2395 = ORIENTED_EDGE('',*,*,#2340,.F.); +#2396 = ORIENTED_EDGE('',*,*,#1281,.T.); +#2397 = ORIENTED_EDGE('',*,*,#2379,.T.); +#2398 = ORIENTED_EDGE('',*,*,#1669,.F.); +#2399 = PLANE('',#2400); +#2400 = AXIS2_PLACEMENT_3D('',#2401,#2402,#2403); +#2401 = CARTESIAN_POINT('',(-400.92,-611.,912.)); +#2402 = DIRECTION('',(1.,0.,-0.)); +#2403 = DIRECTION('',(0.,0.,1.)); +#2404 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2408)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#2405,#2406,#2407)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#2405 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#2406 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#2407 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#2408 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-07),#2405, + 'distance_accuracy_value','confusion accuracy'); +#2409 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2410,#2412); +#2410 = ( REPRESENTATION_RELATIONSHIP('','',#893,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2411) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#2411 = ITEM_DEFINED_TRANSFORMATION('','',#11,#19); +#2412 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #2413); +#2413 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('2','NAU03_Left_Side_Panel','',#5 + ,#888,$); +#2414 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#890)); +#2415 = SHAPE_DEFINITION_REPRESENTATION(#2416,#2422); +#2416 = PRODUCT_DEFINITION_SHAPE('','',#2417); +#2417 = PRODUCT_DEFINITION('design','',#2418,#2421); +#2418 = PRODUCT_DEFINITION_FORMATION('','',#2419); +#2419 = PRODUCT('NAU03_Right_Side_Panel','NAU03_Right_Side_Panel','',( + #2420)); +#2420 = PRODUCT_CONTEXT('',#2,'mechanical'); +#2421 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#2422 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2423),#3933); +#2423 = MANIFOLD_SOLID_BREP('',#2424); +#2424 = CLOSED_SHELL('',(#2425,#2465,#2836,#2860,#3224,#3241,#3253,#3275 + ,#3292,#3309,#3321,#3343,#3360,#3377,#3389,#3411,#3428,#3445,#3457, + #3479,#3496,#3513,#3525,#3547,#3564,#3581,#3593,#3615,#3632,#3649, + #3661,#3683,#3700,#3717,#3729,#3751,#3768,#3785,#3797,#3819,#3836, + #3853,#3865,#3887,#3904,#3921)); +#2425 = ADVANCED_FACE('',(#2426),#2460,.F.); +#2426 = FACE_BOUND('',#2427,.F.); +#2427 = EDGE_LOOP('',(#2428,#2438,#2446,#2454)); +#2428 = ORIENTED_EDGE('',*,*,#2429,.F.); +#2429 = EDGE_CURVE('',#2430,#2432,#2434,.T.); +#2430 = VERTEX_POINT('',#2431); +#2431 = CARTESIAN_POINT('',(375.,-608.,50.)); +#2432 = VERTEX_POINT('',#2433); +#2433 = CARTESIAN_POINT('',(375.,-608.,2.25E+03)); +#2434 = LINE('',#2435,#2436); +#2435 = CARTESIAN_POINT('',(375.,-608.,50.)); +#2436 = VECTOR('',#2437,1.); +#2437 = DIRECTION('',(0.,0.,1.)); +#2438 = ORIENTED_EDGE('',*,*,#2439,.T.); +#2439 = EDGE_CURVE('',#2430,#2440,#2442,.T.); +#2440 = VERTEX_POINT('',#2441); +#2441 = CARTESIAN_POINT('',(375.,608.,50.)); +#2442 = LINE('',#2443,#2444); +#2443 = CARTESIAN_POINT('',(375.,-608.,50.)); +#2444 = VECTOR('',#2445,1.); +#2445 = DIRECTION('',(-0.,1.,0.)); +#2446 = ORIENTED_EDGE('',*,*,#2447,.T.); +#2447 = EDGE_CURVE('',#2440,#2448,#2450,.T.); +#2448 = VERTEX_POINT('',#2449); +#2449 = CARTESIAN_POINT('',(375.,608.,2.25E+03)); +#2450 = LINE('',#2451,#2452); +#2451 = CARTESIAN_POINT('',(375.,608.,50.)); +#2452 = VECTOR('',#2453,1.); +#2453 = DIRECTION('',(0.,0.,1.)); +#2454 = ORIENTED_EDGE('',*,*,#2455,.F.); +#2455 = EDGE_CURVE('',#2432,#2448,#2456,.T.); +#2456 = LINE('',#2457,#2458); +#2457 = CARTESIAN_POINT('',(375.,-608.,2.25E+03)); +#2458 = VECTOR('',#2459,1.); +#2459 = DIRECTION('',(-0.,1.,0.)); +#2460 = PLANE('',#2461); +#2461 = AXIS2_PLACEMENT_3D('',#2462,#2463,#2464); +#2462 = CARTESIAN_POINT('',(375.,-608.,50.)); +#2463 = DIRECTION('',(1.,0.,-0.)); +#2464 = DIRECTION('',(0.,0.,1.)); +#2465 = ADVANCED_FACE('',(#2466,#2491,#2525,#2559,#2593,#2627,#2661, + #2695,#2729,#2763,#2797),#2831,.F.); +#2466 = FACE_BOUND('',#2467,.F.); +#2467 = EDGE_LOOP('',(#2468,#2476,#2477,#2485)); +#2468 = ORIENTED_EDGE('',*,*,#2469,.F.); +#2469 = EDGE_CURVE('',#2430,#2470,#2472,.T.); +#2470 = VERTEX_POINT('',#2471); +#2471 = CARTESIAN_POINT('',(407.,-608.,50.)); +#2472 = LINE('',#2473,#2474); +#2473 = CARTESIAN_POINT('',(375.,-608.,50.)); +#2474 = VECTOR('',#2475,1.); +#2475 = DIRECTION('',(1.,0.,-0.)); +#2476 = ORIENTED_EDGE('',*,*,#2429,.T.); +#2477 = ORIENTED_EDGE('',*,*,#2478,.T.); +#2478 = EDGE_CURVE('',#2432,#2479,#2481,.T.); +#2479 = VERTEX_POINT('',#2480); +#2480 = CARTESIAN_POINT('',(407.,-608.,2.25E+03)); +#2481 = LINE('',#2482,#2483); +#2482 = CARTESIAN_POINT('',(375.,-608.,2.25E+03)); +#2483 = VECTOR('',#2484,1.); +#2484 = DIRECTION('',(1.,0.,-0.)); +#2485 = ORIENTED_EDGE('',*,*,#2486,.F.); +#2486 = EDGE_CURVE('',#2470,#2479,#2487,.T.); +#2487 = LINE('',#2488,#2489); +#2488 = CARTESIAN_POINT('',(407.,-608.,50.)); +#2489 = VECTOR('',#2490,1.); +#2490 = DIRECTION('',(0.,0.,1.)); +#2491 = FACE_BOUND('',#2492,.F.); +#2492 = EDGE_LOOP('',(#2493,#2503,#2511,#2519)); +#2493 = ORIENTED_EDGE('',*,*,#2494,.F.); +#2494 = EDGE_CURVE('',#2495,#2497,#2499,.T.); +#2495 = VERTEX_POINT('',#2496); +#2496 = CARTESIAN_POINT('',(381.08,-608.,550.)); +#2497 = VERTEX_POINT('',#2498); +#2498 = CARTESIAN_POINT('',(400.92,-608.,550.)); +#2499 = LINE('',#2500,#2501); +#2500 = CARTESIAN_POINT('',(378.04,-608.,550.)); +#2501 = VECTOR('',#2502,1.); +#2502 = DIRECTION('',(1.,0.,-0.)); +#2503 = ORIENTED_EDGE('',*,*,#2504,.T.); +#2504 = EDGE_CURVE('',#2495,#2505,#2507,.T.); +#2505 = VERTEX_POINT('',#2506); +#2506 = CARTESIAN_POINT('',(381.08,-608.,534.)); +#2507 = LINE('',#2508,#2509); +#2508 = CARTESIAN_POINT('',(381.08,-608.,292.)); +#2509 = VECTOR('',#2510,1.); +#2510 = DIRECTION('',(-0.,0.,-1.)); +#2511 = ORIENTED_EDGE('',*,*,#2512,.T.); +#2512 = EDGE_CURVE('',#2505,#2513,#2515,.T.); +#2513 = VERTEX_POINT('',#2514); +#2514 = CARTESIAN_POINT('',(400.92,-608.,534.)); +#2515 = LINE('',#2516,#2517); +#2516 = CARTESIAN_POINT('',(378.04,-608.,534.)); +#2517 = VECTOR('',#2518,1.); +#2518 = DIRECTION('',(1.,0.,-0.)); +#2519 = ORIENTED_EDGE('',*,*,#2520,.F.); +#2520 = EDGE_CURVE('',#2497,#2513,#2521,.T.); +#2521 = LINE('',#2522,#2523); +#2522 = CARTESIAN_POINT('',(400.92,-608.,292.)); +#2523 = VECTOR('',#2524,1.); +#2524 = DIRECTION('',(-0.,0.,-1.)); +#2525 = FACE_BOUND('',#2526,.F.); +#2526 = EDGE_LOOP('',(#2527,#2537,#2545,#2553)); +#2527 = ORIENTED_EDGE('',*,*,#2528,.F.); +#2528 = EDGE_CURVE('',#2529,#2531,#2533,.T.); +#2529 = VERTEX_POINT('',#2530); +#2530 = CARTESIAN_POINT('',(381.08,-608.,592.)); +#2531 = VERTEX_POINT('',#2532); +#2532 = CARTESIAN_POINT('',(400.92,-608.,592.)); +#2533 = LINE('',#2534,#2535); +#2534 = CARTESIAN_POINT('',(378.04,-608.,592.)); +#2535 = VECTOR('',#2536,1.); +#2536 = DIRECTION('',(1.,0.,-0.)); +#2537 = ORIENTED_EDGE('',*,*,#2538,.T.); +#2538 = EDGE_CURVE('',#2529,#2539,#2541,.T.); +#2539 = VERTEX_POINT('',#2540); +#2540 = CARTESIAN_POINT('',(381.08,-608.,576.)); +#2541 = LINE('',#2542,#2543); +#2542 = CARTESIAN_POINT('',(381.08,-608.,313.)); +#2543 = VECTOR('',#2544,1.); +#2544 = DIRECTION('',(-0.,0.,-1.)); +#2545 = ORIENTED_EDGE('',*,*,#2546,.T.); +#2546 = EDGE_CURVE('',#2539,#2547,#2549,.T.); +#2547 = VERTEX_POINT('',#2548); +#2548 = CARTESIAN_POINT('',(400.92,-608.,576.)); +#2549 = LINE('',#2550,#2551); +#2550 = CARTESIAN_POINT('',(378.04,-608.,576.)); +#2551 = VECTOR('',#2552,1.); +#2552 = DIRECTION('',(1.,0.,-0.)); +#2553 = ORIENTED_EDGE('',*,*,#2554,.F.); +#2554 = EDGE_CURVE('',#2531,#2547,#2555,.T.); +#2555 = LINE('',#2556,#2557); +#2556 = CARTESIAN_POINT('',(400.92,-608.,313.)); +#2557 = VECTOR('',#2558,1.); +#2558 = DIRECTION('',(-0.,0.,-1.)); +#2559 = FACE_BOUND('',#2560,.F.); +#2560 = EDGE_LOOP('',(#2561,#2571,#2579,#2587)); +#2561 = ORIENTED_EDGE('',*,*,#2562,.F.); +#2562 = EDGE_CURVE('',#2563,#2565,#2567,.T.); +#2563 = VERTEX_POINT('',#2564); +#2564 = CARTESIAN_POINT('',(381.08,-608.,634.)); +#2565 = VERTEX_POINT('',#2566); +#2566 = CARTESIAN_POINT('',(400.92,-608.,634.)); +#2567 = LINE('',#2568,#2569); +#2568 = CARTESIAN_POINT('',(378.04,-608.,634.)); +#2569 = VECTOR('',#2570,1.); +#2570 = DIRECTION('',(1.,0.,-0.)); +#2571 = ORIENTED_EDGE('',*,*,#2572,.T.); +#2572 = EDGE_CURVE('',#2563,#2573,#2575,.T.); +#2573 = VERTEX_POINT('',#2574); +#2574 = CARTESIAN_POINT('',(381.08,-608.,618.)); +#2575 = LINE('',#2576,#2577); +#2576 = CARTESIAN_POINT('',(381.08,-608.,334.)); +#2577 = VECTOR('',#2578,1.); +#2578 = DIRECTION('',(-0.,0.,-1.)); +#2579 = ORIENTED_EDGE('',*,*,#2580,.T.); +#2580 = EDGE_CURVE('',#2573,#2581,#2583,.T.); +#2581 = VERTEX_POINT('',#2582); +#2582 = CARTESIAN_POINT('',(400.92,-608.,618.)); +#2583 = LINE('',#2584,#2585); +#2584 = CARTESIAN_POINT('',(378.04,-608.,618.)); +#2585 = VECTOR('',#2586,1.); +#2586 = DIRECTION('',(1.,0.,-0.)); +#2587 = ORIENTED_EDGE('',*,*,#2588,.F.); +#2588 = EDGE_CURVE('',#2565,#2581,#2589,.T.); +#2589 = LINE('',#2590,#2591); +#2590 = CARTESIAN_POINT('',(400.92,-608.,334.)); +#2591 = VECTOR('',#2592,1.); +#2592 = DIRECTION('',(-0.,0.,-1.)); +#2593 = FACE_BOUND('',#2594,.F.); +#2594 = EDGE_LOOP('',(#2595,#2605,#2613,#2621)); +#2595 = ORIENTED_EDGE('',*,*,#2596,.F.); +#2596 = EDGE_CURVE('',#2597,#2599,#2601,.T.); +#2597 = VERTEX_POINT('',#2598); +#2598 = CARTESIAN_POINT('',(381.08,-608.,676.)); +#2599 = VERTEX_POINT('',#2600); +#2600 = CARTESIAN_POINT('',(400.92,-608.,676.)); +#2601 = LINE('',#2602,#2603); +#2602 = CARTESIAN_POINT('',(378.04,-608.,676.)); +#2603 = VECTOR('',#2604,1.); +#2604 = DIRECTION('',(1.,0.,-0.)); +#2605 = ORIENTED_EDGE('',*,*,#2606,.T.); +#2606 = EDGE_CURVE('',#2597,#2607,#2609,.T.); +#2607 = VERTEX_POINT('',#2608); +#2608 = CARTESIAN_POINT('',(381.08,-608.,660.)); +#2609 = LINE('',#2610,#2611); +#2610 = CARTESIAN_POINT('',(381.08,-608.,355.)); +#2611 = VECTOR('',#2612,1.); +#2612 = DIRECTION('',(-0.,0.,-1.)); +#2613 = ORIENTED_EDGE('',*,*,#2614,.T.); +#2614 = EDGE_CURVE('',#2607,#2615,#2617,.T.); +#2615 = VERTEX_POINT('',#2616); +#2616 = CARTESIAN_POINT('',(400.92,-608.,660.)); +#2617 = LINE('',#2618,#2619); +#2618 = CARTESIAN_POINT('',(378.04,-608.,660.)); +#2619 = VECTOR('',#2620,1.); +#2620 = DIRECTION('',(1.,0.,-0.)); +#2621 = ORIENTED_EDGE('',*,*,#2622,.F.); +#2622 = EDGE_CURVE('',#2599,#2615,#2623,.T.); +#2623 = LINE('',#2624,#2625); +#2624 = CARTESIAN_POINT('',(400.92,-608.,355.)); +#2625 = VECTOR('',#2626,1.); +#2626 = DIRECTION('',(-0.,0.,-1.)); +#2627 = FACE_BOUND('',#2628,.F.); +#2628 = EDGE_LOOP('',(#2629,#2639,#2647,#2655)); +#2629 = ORIENTED_EDGE('',*,*,#2630,.F.); +#2630 = EDGE_CURVE('',#2631,#2633,#2635,.T.); +#2631 = VERTEX_POINT('',#2632); +#2632 = CARTESIAN_POINT('',(381.08,-608.,718.)); +#2633 = VERTEX_POINT('',#2634); +#2634 = CARTESIAN_POINT('',(400.92,-608.,718.)); +#2635 = LINE('',#2636,#2637); +#2636 = CARTESIAN_POINT('',(378.04,-608.,718.)); +#2637 = VECTOR('',#2638,1.); +#2638 = DIRECTION('',(1.,0.,-0.)); +#2639 = ORIENTED_EDGE('',*,*,#2640,.T.); +#2640 = EDGE_CURVE('',#2631,#2641,#2643,.T.); +#2641 = VERTEX_POINT('',#2642); +#2642 = CARTESIAN_POINT('',(381.08,-608.,702.)); +#2643 = LINE('',#2644,#2645); +#2644 = CARTESIAN_POINT('',(381.08,-608.,376.)); +#2645 = VECTOR('',#2646,1.); +#2646 = DIRECTION('',(-0.,0.,-1.)); +#2647 = ORIENTED_EDGE('',*,*,#2648,.T.); +#2648 = EDGE_CURVE('',#2641,#2649,#2651,.T.); +#2649 = VERTEX_POINT('',#2650); +#2650 = CARTESIAN_POINT('',(400.92,-608.,702.)); +#2651 = LINE('',#2652,#2653); +#2652 = CARTESIAN_POINT('',(378.04,-608.,702.)); +#2653 = VECTOR('',#2654,1.); +#2654 = DIRECTION('',(1.,0.,-0.)); +#2655 = ORIENTED_EDGE('',*,*,#2656,.F.); +#2656 = EDGE_CURVE('',#2633,#2649,#2657,.T.); +#2657 = LINE('',#2658,#2659); +#2658 = CARTESIAN_POINT('',(400.92,-608.,376.)); +#2659 = VECTOR('',#2660,1.); +#2660 = DIRECTION('',(-0.,0.,-1.)); +#2661 = FACE_BOUND('',#2662,.F.); +#2662 = EDGE_LOOP('',(#2663,#2673,#2681,#2689)); +#2663 = ORIENTED_EDGE('',*,*,#2664,.F.); +#2664 = EDGE_CURVE('',#2665,#2667,#2669,.T.); +#2665 = VERTEX_POINT('',#2666); +#2666 = CARTESIAN_POINT('',(381.08,-608.,760.)); +#2667 = VERTEX_POINT('',#2668); +#2668 = CARTESIAN_POINT('',(400.92,-608.,760.)); +#2669 = LINE('',#2670,#2671); +#2670 = CARTESIAN_POINT('',(378.04,-608.,760.)); +#2671 = VECTOR('',#2672,1.); +#2672 = DIRECTION('',(1.,0.,-0.)); +#2673 = ORIENTED_EDGE('',*,*,#2674,.T.); +#2674 = EDGE_CURVE('',#2665,#2675,#2677,.T.); +#2675 = VERTEX_POINT('',#2676); +#2676 = CARTESIAN_POINT('',(381.08,-608.,744.)); +#2677 = LINE('',#2678,#2679); +#2678 = CARTESIAN_POINT('',(381.08,-608.,397.)); +#2679 = VECTOR('',#2680,1.); +#2680 = DIRECTION('',(-0.,0.,-1.)); +#2681 = ORIENTED_EDGE('',*,*,#2682,.T.); +#2682 = EDGE_CURVE('',#2675,#2683,#2685,.T.); +#2683 = VERTEX_POINT('',#2684); +#2684 = CARTESIAN_POINT('',(400.92,-608.,744.)); +#2685 = LINE('',#2686,#2687); +#2686 = CARTESIAN_POINT('',(378.04,-608.,744.)); +#2687 = VECTOR('',#2688,1.); +#2688 = DIRECTION('',(1.,0.,-0.)); +#2689 = ORIENTED_EDGE('',*,*,#2690,.F.); +#2690 = EDGE_CURVE('',#2667,#2683,#2691,.T.); +#2691 = LINE('',#2692,#2693); +#2692 = CARTESIAN_POINT('',(400.92,-608.,397.)); +#2693 = VECTOR('',#2694,1.); +#2694 = DIRECTION('',(-0.,0.,-1.)); +#2695 = FACE_BOUND('',#2696,.F.); +#2696 = EDGE_LOOP('',(#2697,#2707,#2715,#2723)); +#2697 = ORIENTED_EDGE('',*,*,#2698,.F.); +#2698 = EDGE_CURVE('',#2699,#2701,#2703,.T.); +#2699 = VERTEX_POINT('',#2700); +#2700 = CARTESIAN_POINT('',(381.08,-608.,802.)); +#2701 = VERTEX_POINT('',#2702); +#2702 = CARTESIAN_POINT('',(400.92,-608.,802.)); +#2703 = LINE('',#2704,#2705); +#2704 = CARTESIAN_POINT('',(378.04,-608.,802.)); +#2705 = VECTOR('',#2706,1.); +#2706 = DIRECTION('',(1.,0.,-0.)); +#2707 = ORIENTED_EDGE('',*,*,#2708,.T.); +#2708 = EDGE_CURVE('',#2699,#2709,#2711,.T.); +#2709 = VERTEX_POINT('',#2710); +#2710 = CARTESIAN_POINT('',(381.08,-608.,786.)); +#2711 = LINE('',#2712,#2713); +#2712 = CARTESIAN_POINT('',(381.08,-608.,418.)); +#2713 = VECTOR('',#2714,1.); +#2714 = DIRECTION('',(-0.,0.,-1.)); +#2715 = ORIENTED_EDGE('',*,*,#2716,.T.); +#2716 = EDGE_CURVE('',#2709,#2717,#2719,.T.); +#2717 = VERTEX_POINT('',#2718); +#2718 = CARTESIAN_POINT('',(400.92,-608.,786.)); +#2719 = LINE('',#2720,#2721); +#2720 = CARTESIAN_POINT('',(378.04,-608.,786.)); +#2721 = VECTOR('',#2722,1.); +#2722 = DIRECTION('',(1.,0.,-0.)); +#2723 = ORIENTED_EDGE('',*,*,#2724,.F.); +#2724 = EDGE_CURVE('',#2701,#2717,#2725,.T.); +#2725 = LINE('',#2726,#2727); +#2726 = CARTESIAN_POINT('',(400.92,-608.,418.)); +#2727 = VECTOR('',#2728,1.); +#2728 = DIRECTION('',(-0.,0.,-1.)); +#2729 = FACE_BOUND('',#2730,.F.); +#2730 = EDGE_LOOP('',(#2731,#2741,#2749,#2757)); +#2731 = ORIENTED_EDGE('',*,*,#2732,.F.); +#2732 = EDGE_CURVE('',#2733,#2735,#2737,.T.); +#2733 = VERTEX_POINT('',#2734); +#2734 = CARTESIAN_POINT('',(381.08,-608.,844.)); +#2735 = VERTEX_POINT('',#2736); +#2736 = CARTESIAN_POINT('',(400.92,-608.,844.)); +#2737 = LINE('',#2738,#2739); +#2738 = CARTESIAN_POINT('',(378.04,-608.,844.)); +#2739 = VECTOR('',#2740,1.); +#2740 = DIRECTION('',(1.,0.,-0.)); +#2741 = ORIENTED_EDGE('',*,*,#2742,.T.); +#2742 = EDGE_CURVE('',#2733,#2743,#2745,.T.); +#2743 = VERTEX_POINT('',#2744); +#2744 = CARTESIAN_POINT('',(381.08,-608.,828.)); +#2745 = LINE('',#2746,#2747); +#2746 = CARTESIAN_POINT('',(381.08,-608.,439.)); +#2747 = VECTOR('',#2748,1.); +#2748 = DIRECTION('',(-0.,0.,-1.)); +#2749 = ORIENTED_EDGE('',*,*,#2750,.T.); +#2750 = EDGE_CURVE('',#2743,#2751,#2753,.T.); +#2751 = VERTEX_POINT('',#2752); +#2752 = CARTESIAN_POINT('',(400.92,-608.,828.)); +#2753 = LINE('',#2754,#2755); +#2754 = CARTESIAN_POINT('',(378.04,-608.,828.)); +#2755 = VECTOR('',#2756,1.); +#2756 = DIRECTION('',(1.,0.,-0.)); +#2757 = ORIENTED_EDGE('',*,*,#2758,.F.); +#2758 = EDGE_CURVE('',#2735,#2751,#2759,.T.); +#2759 = LINE('',#2760,#2761); +#2760 = CARTESIAN_POINT('',(400.92,-608.,439.)); +#2761 = VECTOR('',#2762,1.); +#2762 = DIRECTION('',(-0.,0.,-1.)); +#2763 = FACE_BOUND('',#2764,.F.); +#2764 = EDGE_LOOP('',(#2765,#2775,#2783,#2791)); +#2765 = ORIENTED_EDGE('',*,*,#2766,.F.); +#2766 = EDGE_CURVE('',#2767,#2769,#2771,.T.); +#2767 = VERTEX_POINT('',#2768); +#2768 = CARTESIAN_POINT('',(381.08,-608.,886.)); +#2769 = VERTEX_POINT('',#2770); +#2770 = CARTESIAN_POINT('',(400.92,-608.,886.)); +#2771 = LINE('',#2772,#2773); +#2772 = CARTESIAN_POINT('',(378.04,-608.,886.)); +#2773 = VECTOR('',#2774,1.); +#2774 = DIRECTION('',(1.,0.,-0.)); +#2775 = ORIENTED_EDGE('',*,*,#2776,.T.); +#2776 = EDGE_CURVE('',#2767,#2777,#2779,.T.); +#2777 = VERTEX_POINT('',#2778); +#2778 = CARTESIAN_POINT('',(381.08,-608.,870.)); +#2779 = LINE('',#2780,#2781); +#2780 = CARTESIAN_POINT('',(381.08,-608.,460.)); +#2781 = VECTOR('',#2782,1.); +#2782 = DIRECTION('',(-0.,0.,-1.)); +#2783 = ORIENTED_EDGE('',*,*,#2784,.T.); +#2784 = EDGE_CURVE('',#2777,#2785,#2787,.T.); +#2785 = VERTEX_POINT('',#2786); +#2786 = CARTESIAN_POINT('',(400.92,-608.,870.)); +#2787 = LINE('',#2788,#2789); +#2788 = CARTESIAN_POINT('',(378.04,-608.,870.)); +#2789 = VECTOR('',#2790,1.); +#2790 = DIRECTION('',(1.,0.,-0.)); +#2791 = ORIENTED_EDGE('',*,*,#2792,.F.); +#2792 = EDGE_CURVE('',#2769,#2785,#2793,.T.); +#2793 = LINE('',#2794,#2795); +#2794 = CARTESIAN_POINT('',(400.92,-608.,460.)); +#2795 = VECTOR('',#2796,1.); +#2796 = DIRECTION('',(-0.,0.,-1.)); +#2797 = FACE_BOUND('',#2798,.F.); +#2798 = EDGE_LOOP('',(#2799,#2809,#2817,#2825)); +#2799 = ORIENTED_EDGE('',*,*,#2800,.F.); +#2800 = EDGE_CURVE('',#2801,#2803,#2805,.T.); +#2801 = VERTEX_POINT('',#2802); +#2802 = CARTESIAN_POINT('',(381.08,-608.,928.)); +#2803 = VERTEX_POINT('',#2804); +#2804 = CARTESIAN_POINT('',(400.92,-608.,928.)); +#2805 = LINE('',#2806,#2807); +#2806 = CARTESIAN_POINT('',(378.04,-608.,928.)); +#2807 = VECTOR('',#2808,1.); +#2808 = DIRECTION('',(1.,0.,-0.)); +#2809 = ORIENTED_EDGE('',*,*,#2810,.T.); +#2810 = EDGE_CURVE('',#2801,#2811,#2813,.T.); +#2811 = VERTEX_POINT('',#2812); +#2812 = CARTESIAN_POINT('',(381.08,-608.,912.)); +#2813 = LINE('',#2814,#2815); +#2814 = CARTESIAN_POINT('',(381.08,-608.,481.)); +#2815 = VECTOR('',#2816,1.); +#2816 = DIRECTION('',(-0.,0.,-1.)); +#2817 = ORIENTED_EDGE('',*,*,#2818,.T.); +#2818 = EDGE_CURVE('',#2811,#2819,#2821,.T.); +#2819 = VERTEX_POINT('',#2820); +#2820 = CARTESIAN_POINT('',(400.92,-608.,912.)); +#2821 = LINE('',#2822,#2823); +#2822 = CARTESIAN_POINT('',(378.04,-608.,912.)); +#2823 = VECTOR('',#2824,1.); +#2824 = DIRECTION('',(1.,0.,-0.)); +#2825 = ORIENTED_EDGE('',*,*,#2826,.F.); +#2826 = EDGE_CURVE('',#2803,#2819,#2827,.T.); +#2827 = LINE('',#2828,#2829); +#2828 = CARTESIAN_POINT('',(400.92,-608.,481.)); +#2829 = VECTOR('',#2830,1.); +#2830 = DIRECTION('',(-0.,0.,-1.)); +#2831 = PLANE('',#2832); +#2832 = AXIS2_PLACEMENT_3D('',#2833,#2834,#2835); +#2833 = CARTESIAN_POINT('',(375.,-608.,50.)); +#2834 = DIRECTION('',(-0.,1.,0.)); +#2835 = DIRECTION('',(0.,0.,1.)); +#2836 = ADVANCED_FACE('',(#2837),#2855,.T.); +#2837 = FACE_BOUND('',#2838,.T.); +#2838 = EDGE_LOOP('',(#2839,#2840,#2841,#2849)); +#2839 = ORIENTED_EDGE('',*,*,#2455,.F.); +#2840 = ORIENTED_EDGE('',*,*,#2478,.T.); +#2841 = ORIENTED_EDGE('',*,*,#2842,.T.); +#2842 = EDGE_CURVE('',#2479,#2843,#2845,.T.); +#2843 = VERTEX_POINT('',#2844); +#2844 = CARTESIAN_POINT('',(407.,608.,2.25E+03)); +#2845 = LINE('',#2846,#2847); +#2846 = CARTESIAN_POINT('',(407.,-608.,2.25E+03)); +#2847 = VECTOR('',#2848,1.); +#2848 = DIRECTION('',(-0.,1.,0.)); +#2849 = ORIENTED_EDGE('',*,*,#2850,.F.); +#2850 = EDGE_CURVE('',#2448,#2843,#2851,.T.); +#2851 = LINE('',#2852,#2853); +#2852 = CARTESIAN_POINT('',(375.,608.,2.25E+03)); +#2853 = VECTOR('',#2854,1.); +#2854 = DIRECTION('',(1.,0.,-0.)); +#2855 = PLANE('',#2856); +#2856 = AXIS2_PLACEMENT_3D('',#2857,#2858,#2859); +#2857 = CARTESIAN_POINT('',(375.,-608.,2.25E+03)); +#2858 = DIRECTION('',(0.,0.,1.)); +#2859 = DIRECTION('',(1.,0.,-0.)); +#2860 = ADVANCED_FACE('',(#2861,#2879,#2913,#2947,#2981,#3015,#3049, + #3083,#3117,#3151,#3185),#3219,.T.); +#2861 = FACE_BOUND('',#2862,.T.); +#2862 = EDGE_LOOP('',(#2863,#2871,#2872,#2873)); +#2863 = ORIENTED_EDGE('',*,*,#2864,.F.); +#2864 = EDGE_CURVE('',#2440,#2865,#2867,.T.); +#2865 = VERTEX_POINT('',#2866); +#2866 = CARTESIAN_POINT('',(407.,608.,50.)); +#2867 = LINE('',#2868,#2869); +#2868 = CARTESIAN_POINT('',(375.,608.,50.)); +#2869 = VECTOR('',#2870,1.); +#2870 = DIRECTION('',(1.,0.,-0.)); +#2871 = ORIENTED_EDGE('',*,*,#2447,.T.); +#2872 = ORIENTED_EDGE('',*,*,#2850,.T.); +#2873 = ORIENTED_EDGE('',*,*,#2874,.F.); +#2874 = EDGE_CURVE('',#2865,#2843,#2875,.T.); +#2875 = LINE('',#2876,#2877); +#2876 = CARTESIAN_POINT('',(407.,608.,50.)); +#2877 = VECTOR('',#2878,1.); +#2878 = DIRECTION('',(0.,0.,1.)); +#2879 = FACE_BOUND('',#2880,.T.); +#2880 = EDGE_LOOP('',(#2881,#2891,#2899,#2907)); +#2881 = ORIENTED_EDGE('',*,*,#2882,.F.); +#2882 = EDGE_CURVE('',#2883,#2885,#2887,.T.); +#2883 = VERTEX_POINT('',#2884); +#2884 = CARTESIAN_POINT('',(381.08,608.,550.)); +#2885 = VERTEX_POINT('',#2886); +#2886 = CARTESIAN_POINT('',(400.92,608.,550.)); +#2887 = LINE('',#2888,#2889); +#2888 = CARTESIAN_POINT('',(378.04,608.,550.)); +#2889 = VECTOR('',#2890,1.); +#2890 = DIRECTION('',(1.,0.,-0.)); +#2891 = ORIENTED_EDGE('',*,*,#2892,.T.); +#2892 = EDGE_CURVE('',#2883,#2893,#2895,.T.); +#2893 = VERTEX_POINT('',#2894); +#2894 = CARTESIAN_POINT('',(381.08,608.,534.)); +#2895 = LINE('',#2896,#2897); +#2896 = CARTESIAN_POINT('',(381.08,608.,292.)); +#2897 = VECTOR('',#2898,1.); +#2898 = DIRECTION('',(-0.,0.,-1.)); +#2899 = ORIENTED_EDGE('',*,*,#2900,.T.); +#2900 = EDGE_CURVE('',#2893,#2901,#2903,.T.); +#2901 = VERTEX_POINT('',#2902); +#2902 = CARTESIAN_POINT('',(400.92,608.,534.)); +#2903 = LINE('',#2904,#2905); +#2904 = CARTESIAN_POINT('',(378.04,608.,534.)); +#2905 = VECTOR('',#2906,1.); +#2906 = DIRECTION('',(1.,0.,-0.)); +#2907 = ORIENTED_EDGE('',*,*,#2908,.F.); +#2908 = EDGE_CURVE('',#2885,#2901,#2909,.T.); +#2909 = LINE('',#2910,#2911); +#2910 = CARTESIAN_POINT('',(400.92,608.,292.)); +#2911 = VECTOR('',#2912,1.); +#2912 = DIRECTION('',(-0.,0.,-1.)); +#2913 = FACE_BOUND('',#2914,.T.); +#2914 = EDGE_LOOP('',(#2915,#2925,#2933,#2941)); +#2915 = ORIENTED_EDGE('',*,*,#2916,.F.); +#2916 = EDGE_CURVE('',#2917,#2919,#2921,.T.); +#2917 = VERTEX_POINT('',#2918); +#2918 = CARTESIAN_POINT('',(381.08,608.,592.)); +#2919 = VERTEX_POINT('',#2920); +#2920 = CARTESIAN_POINT('',(400.92,608.,592.)); +#2921 = LINE('',#2922,#2923); +#2922 = CARTESIAN_POINT('',(378.04,608.,592.)); +#2923 = VECTOR('',#2924,1.); +#2924 = DIRECTION('',(1.,0.,-0.)); +#2925 = ORIENTED_EDGE('',*,*,#2926,.T.); +#2926 = EDGE_CURVE('',#2917,#2927,#2929,.T.); +#2927 = VERTEX_POINT('',#2928); +#2928 = CARTESIAN_POINT('',(381.08,608.,576.)); +#2929 = LINE('',#2930,#2931); +#2930 = CARTESIAN_POINT('',(381.08,608.,313.)); +#2931 = VECTOR('',#2932,1.); +#2932 = DIRECTION('',(-0.,0.,-1.)); +#2933 = ORIENTED_EDGE('',*,*,#2934,.T.); +#2934 = EDGE_CURVE('',#2927,#2935,#2937,.T.); +#2935 = VERTEX_POINT('',#2936); +#2936 = CARTESIAN_POINT('',(400.92,608.,576.)); +#2937 = LINE('',#2938,#2939); +#2938 = CARTESIAN_POINT('',(378.04,608.,576.)); +#2939 = VECTOR('',#2940,1.); +#2940 = DIRECTION('',(1.,0.,-0.)); +#2941 = ORIENTED_EDGE('',*,*,#2942,.F.); +#2942 = EDGE_CURVE('',#2919,#2935,#2943,.T.); +#2943 = LINE('',#2944,#2945); +#2944 = CARTESIAN_POINT('',(400.92,608.,313.)); +#2945 = VECTOR('',#2946,1.); +#2946 = DIRECTION('',(-0.,0.,-1.)); +#2947 = FACE_BOUND('',#2948,.T.); +#2948 = EDGE_LOOP('',(#2949,#2959,#2967,#2975)); +#2949 = ORIENTED_EDGE('',*,*,#2950,.F.); +#2950 = EDGE_CURVE('',#2951,#2953,#2955,.T.); +#2951 = VERTEX_POINT('',#2952); +#2952 = CARTESIAN_POINT('',(381.08,608.,634.)); +#2953 = VERTEX_POINT('',#2954); +#2954 = CARTESIAN_POINT('',(400.92,608.,634.)); +#2955 = LINE('',#2956,#2957); +#2956 = CARTESIAN_POINT('',(378.04,608.,634.)); +#2957 = VECTOR('',#2958,1.); +#2958 = DIRECTION('',(1.,0.,-0.)); +#2959 = ORIENTED_EDGE('',*,*,#2960,.T.); +#2960 = EDGE_CURVE('',#2951,#2961,#2963,.T.); +#2961 = VERTEX_POINT('',#2962); +#2962 = CARTESIAN_POINT('',(381.08,608.,618.)); +#2963 = LINE('',#2964,#2965); +#2964 = CARTESIAN_POINT('',(381.08,608.,334.)); +#2965 = VECTOR('',#2966,1.); +#2966 = DIRECTION('',(-0.,0.,-1.)); +#2967 = ORIENTED_EDGE('',*,*,#2968,.T.); +#2968 = EDGE_CURVE('',#2961,#2969,#2971,.T.); +#2969 = VERTEX_POINT('',#2970); +#2970 = CARTESIAN_POINT('',(400.92,608.,618.)); +#2971 = LINE('',#2972,#2973); +#2972 = CARTESIAN_POINT('',(378.04,608.,618.)); +#2973 = VECTOR('',#2974,1.); +#2974 = DIRECTION('',(1.,0.,-0.)); +#2975 = ORIENTED_EDGE('',*,*,#2976,.F.); +#2976 = EDGE_CURVE('',#2953,#2969,#2977,.T.); +#2977 = LINE('',#2978,#2979); +#2978 = CARTESIAN_POINT('',(400.92,608.,334.)); +#2979 = VECTOR('',#2980,1.); +#2980 = DIRECTION('',(-0.,0.,-1.)); +#2981 = FACE_BOUND('',#2982,.T.); +#2982 = EDGE_LOOP('',(#2983,#2993,#3001,#3009)); +#2983 = ORIENTED_EDGE('',*,*,#2984,.F.); +#2984 = EDGE_CURVE('',#2985,#2987,#2989,.T.); +#2985 = VERTEX_POINT('',#2986); +#2986 = CARTESIAN_POINT('',(381.08,608.,676.)); +#2987 = VERTEX_POINT('',#2988); +#2988 = CARTESIAN_POINT('',(400.92,608.,676.)); +#2989 = LINE('',#2990,#2991); +#2990 = CARTESIAN_POINT('',(378.04,608.,676.)); +#2991 = VECTOR('',#2992,1.); +#2992 = DIRECTION('',(1.,0.,-0.)); +#2993 = ORIENTED_EDGE('',*,*,#2994,.T.); +#2994 = EDGE_CURVE('',#2985,#2995,#2997,.T.); +#2995 = VERTEX_POINT('',#2996); +#2996 = CARTESIAN_POINT('',(381.08,608.,660.)); +#2997 = LINE('',#2998,#2999); +#2998 = CARTESIAN_POINT('',(381.08,608.,355.)); +#2999 = VECTOR('',#3000,1.); +#3000 = DIRECTION('',(-0.,0.,-1.)); +#3001 = ORIENTED_EDGE('',*,*,#3002,.T.); +#3002 = EDGE_CURVE('',#2995,#3003,#3005,.T.); +#3003 = VERTEX_POINT('',#3004); +#3004 = CARTESIAN_POINT('',(400.92,608.,660.)); +#3005 = LINE('',#3006,#3007); +#3006 = CARTESIAN_POINT('',(378.04,608.,660.)); +#3007 = VECTOR('',#3008,1.); +#3008 = DIRECTION('',(1.,0.,-0.)); +#3009 = ORIENTED_EDGE('',*,*,#3010,.F.); +#3010 = EDGE_CURVE('',#2987,#3003,#3011,.T.); +#3011 = LINE('',#3012,#3013); +#3012 = CARTESIAN_POINT('',(400.92,608.,355.)); +#3013 = VECTOR('',#3014,1.); +#3014 = DIRECTION('',(-0.,0.,-1.)); +#3015 = FACE_BOUND('',#3016,.T.); +#3016 = EDGE_LOOP('',(#3017,#3027,#3035,#3043)); +#3017 = ORIENTED_EDGE('',*,*,#3018,.F.); +#3018 = EDGE_CURVE('',#3019,#3021,#3023,.T.); +#3019 = VERTEX_POINT('',#3020); +#3020 = CARTESIAN_POINT('',(381.08,608.,718.)); +#3021 = VERTEX_POINT('',#3022); +#3022 = CARTESIAN_POINT('',(400.92,608.,718.)); +#3023 = LINE('',#3024,#3025); +#3024 = CARTESIAN_POINT('',(378.04,608.,718.)); +#3025 = VECTOR('',#3026,1.); +#3026 = DIRECTION('',(1.,0.,-0.)); +#3027 = ORIENTED_EDGE('',*,*,#3028,.T.); +#3028 = EDGE_CURVE('',#3019,#3029,#3031,.T.); +#3029 = VERTEX_POINT('',#3030); +#3030 = CARTESIAN_POINT('',(381.08,608.,702.)); +#3031 = LINE('',#3032,#3033); +#3032 = CARTESIAN_POINT('',(381.08,608.,376.)); +#3033 = VECTOR('',#3034,1.); +#3034 = DIRECTION('',(-0.,0.,-1.)); +#3035 = ORIENTED_EDGE('',*,*,#3036,.T.); +#3036 = EDGE_CURVE('',#3029,#3037,#3039,.T.); +#3037 = VERTEX_POINT('',#3038); +#3038 = CARTESIAN_POINT('',(400.92,608.,702.)); +#3039 = LINE('',#3040,#3041); +#3040 = CARTESIAN_POINT('',(378.04,608.,702.)); +#3041 = VECTOR('',#3042,1.); +#3042 = DIRECTION('',(1.,0.,-0.)); +#3043 = ORIENTED_EDGE('',*,*,#3044,.F.); +#3044 = EDGE_CURVE('',#3021,#3037,#3045,.T.); +#3045 = LINE('',#3046,#3047); +#3046 = CARTESIAN_POINT('',(400.92,608.,376.)); +#3047 = VECTOR('',#3048,1.); +#3048 = DIRECTION('',(-0.,0.,-1.)); +#3049 = FACE_BOUND('',#3050,.T.); +#3050 = EDGE_LOOP('',(#3051,#3061,#3069,#3077)); +#3051 = ORIENTED_EDGE('',*,*,#3052,.F.); +#3052 = EDGE_CURVE('',#3053,#3055,#3057,.T.); +#3053 = VERTEX_POINT('',#3054); +#3054 = CARTESIAN_POINT('',(381.08,608.,760.)); +#3055 = VERTEX_POINT('',#3056); +#3056 = CARTESIAN_POINT('',(400.92,608.,760.)); +#3057 = LINE('',#3058,#3059); +#3058 = CARTESIAN_POINT('',(378.04,608.,760.)); +#3059 = VECTOR('',#3060,1.); +#3060 = DIRECTION('',(1.,0.,-0.)); +#3061 = ORIENTED_EDGE('',*,*,#3062,.T.); +#3062 = EDGE_CURVE('',#3053,#3063,#3065,.T.); +#3063 = VERTEX_POINT('',#3064); +#3064 = CARTESIAN_POINT('',(381.08,608.,744.)); +#3065 = LINE('',#3066,#3067); +#3066 = CARTESIAN_POINT('',(381.08,608.,397.)); +#3067 = VECTOR('',#3068,1.); +#3068 = DIRECTION('',(-0.,0.,-1.)); +#3069 = ORIENTED_EDGE('',*,*,#3070,.T.); +#3070 = EDGE_CURVE('',#3063,#3071,#3073,.T.); +#3071 = VERTEX_POINT('',#3072); +#3072 = CARTESIAN_POINT('',(400.92,608.,744.)); +#3073 = LINE('',#3074,#3075); +#3074 = CARTESIAN_POINT('',(378.04,608.,744.)); +#3075 = VECTOR('',#3076,1.); +#3076 = DIRECTION('',(1.,0.,-0.)); +#3077 = ORIENTED_EDGE('',*,*,#3078,.F.); +#3078 = EDGE_CURVE('',#3055,#3071,#3079,.T.); +#3079 = LINE('',#3080,#3081); +#3080 = CARTESIAN_POINT('',(400.92,608.,397.)); +#3081 = VECTOR('',#3082,1.); +#3082 = DIRECTION('',(-0.,0.,-1.)); +#3083 = FACE_BOUND('',#3084,.T.); +#3084 = EDGE_LOOP('',(#3085,#3095,#3103,#3111)); +#3085 = ORIENTED_EDGE('',*,*,#3086,.F.); +#3086 = EDGE_CURVE('',#3087,#3089,#3091,.T.); +#3087 = VERTEX_POINT('',#3088); +#3088 = CARTESIAN_POINT('',(381.08,608.,802.)); +#3089 = VERTEX_POINT('',#3090); +#3090 = CARTESIAN_POINT('',(400.92,608.,802.)); +#3091 = LINE('',#3092,#3093); +#3092 = CARTESIAN_POINT('',(378.04,608.,802.)); +#3093 = VECTOR('',#3094,1.); +#3094 = DIRECTION('',(1.,0.,-0.)); +#3095 = ORIENTED_EDGE('',*,*,#3096,.T.); +#3096 = EDGE_CURVE('',#3087,#3097,#3099,.T.); +#3097 = VERTEX_POINT('',#3098); +#3098 = CARTESIAN_POINT('',(381.08,608.,786.)); +#3099 = LINE('',#3100,#3101); +#3100 = CARTESIAN_POINT('',(381.08,608.,418.)); +#3101 = VECTOR('',#3102,1.); +#3102 = DIRECTION('',(-0.,0.,-1.)); +#3103 = ORIENTED_EDGE('',*,*,#3104,.T.); +#3104 = EDGE_CURVE('',#3097,#3105,#3107,.T.); +#3105 = VERTEX_POINT('',#3106); +#3106 = CARTESIAN_POINT('',(400.92,608.,786.)); +#3107 = LINE('',#3108,#3109); +#3108 = CARTESIAN_POINT('',(378.04,608.,786.)); +#3109 = VECTOR('',#3110,1.); +#3110 = DIRECTION('',(1.,0.,-0.)); +#3111 = ORIENTED_EDGE('',*,*,#3112,.F.); +#3112 = EDGE_CURVE('',#3089,#3105,#3113,.T.); +#3113 = LINE('',#3114,#3115); +#3114 = CARTESIAN_POINT('',(400.92,608.,418.)); +#3115 = VECTOR('',#3116,1.); +#3116 = DIRECTION('',(-0.,0.,-1.)); +#3117 = FACE_BOUND('',#3118,.T.); +#3118 = EDGE_LOOP('',(#3119,#3129,#3137,#3145)); +#3119 = ORIENTED_EDGE('',*,*,#3120,.F.); +#3120 = EDGE_CURVE('',#3121,#3123,#3125,.T.); +#3121 = VERTEX_POINT('',#3122); +#3122 = CARTESIAN_POINT('',(381.08,608.,844.)); +#3123 = VERTEX_POINT('',#3124); +#3124 = CARTESIAN_POINT('',(400.92,608.,844.)); +#3125 = LINE('',#3126,#3127); +#3126 = CARTESIAN_POINT('',(378.04,608.,844.)); +#3127 = VECTOR('',#3128,1.); +#3128 = DIRECTION('',(1.,0.,-0.)); +#3129 = ORIENTED_EDGE('',*,*,#3130,.T.); +#3130 = EDGE_CURVE('',#3121,#3131,#3133,.T.); +#3131 = VERTEX_POINT('',#3132); +#3132 = CARTESIAN_POINT('',(381.08,608.,828.)); +#3133 = LINE('',#3134,#3135); +#3134 = CARTESIAN_POINT('',(381.08,608.,439.)); +#3135 = VECTOR('',#3136,1.); +#3136 = DIRECTION('',(-0.,0.,-1.)); +#3137 = ORIENTED_EDGE('',*,*,#3138,.T.); +#3138 = EDGE_CURVE('',#3131,#3139,#3141,.T.); +#3139 = VERTEX_POINT('',#3140); +#3140 = CARTESIAN_POINT('',(400.92,608.,828.)); +#3141 = LINE('',#3142,#3143); +#3142 = CARTESIAN_POINT('',(378.04,608.,828.)); +#3143 = VECTOR('',#3144,1.); +#3144 = DIRECTION('',(1.,0.,-0.)); +#3145 = ORIENTED_EDGE('',*,*,#3146,.F.); +#3146 = EDGE_CURVE('',#3123,#3139,#3147,.T.); +#3147 = LINE('',#3148,#3149); +#3148 = CARTESIAN_POINT('',(400.92,608.,439.)); +#3149 = VECTOR('',#3150,1.); +#3150 = DIRECTION('',(-0.,0.,-1.)); +#3151 = FACE_BOUND('',#3152,.T.); +#3152 = EDGE_LOOP('',(#3153,#3163,#3171,#3179)); +#3153 = ORIENTED_EDGE('',*,*,#3154,.F.); +#3154 = EDGE_CURVE('',#3155,#3157,#3159,.T.); +#3155 = VERTEX_POINT('',#3156); +#3156 = CARTESIAN_POINT('',(381.08,608.,886.)); +#3157 = VERTEX_POINT('',#3158); +#3158 = CARTESIAN_POINT('',(400.92,608.,886.)); +#3159 = LINE('',#3160,#3161); +#3160 = CARTESIAN_POINT('',(378.04,608.,886.)); +#3161 = VECTOR('',#3162,1.); +#3162 = DIRECTION('',(1.,0.,-0.)); +#3163 = ORIENTED_EDGE('',*,*,#3164,.T.); +#3164 = EDGE_CURVE('',#3155,#3165,#3167,.T.); +#3165 = VERTEX_POINT('',#3166); +#3166 = CARTESIAN_POINT('',(381.08,608.,870.)); +#3167 = LINE('',#3168,#3169); +#3168 = CARTESIAN_POINT('',(381.08,608.,460.)); +#3169 = VECTOR('',#3170,1.); +#3170 = DIRECTION('',(-0.,0.,-1.)); +#3171 = ORIENTED_EDGE('',*,*,#3172,.T.); +#3172 = EDGE_CURVE('',#3165,#3173,#3175,.T.); +#3173 = VERTEX_POINT('',#3174); +#3174 = CARTESIAN_POINT('',(400.92,608.,870.)); +#3175 = LINE('',#3176,#3177); +#3176 = CARTESIAN_POINT('',(378.04,608.,870.)); +#3177 = VECTOR('',#3178,1.); +#3178 = DIRECTION('',(1.,0.,-0.)); +#3179 = ORIENTED_EDGE('',*,*,#3180,.F.); +#3180 = EDGE_CURVE('',#3157,#3173,#3181,.T.); +#3181 = LINE('',#3182,#3183); +#3182 = CARTESIAN_POINT('',(400.92,608.,460.)); +#3183 = VECTOR('',#3184,1.); +#3184 = DIRECTION('',(-0.,0.,-1.)); +#3185 = FACE_BOUND('',#3186,.T.); +#3186 = EDGE_LOOP('',(#3187,#3197,#3205,#3213)); +#3187 = ORIENTED_EDGE('',*,*,#3188,.F.); +#3188 = EDGE_CURVE('',#3189,#3191,#3193,.T.); +#3189 = VERTEX_POINT('',#3190); +#3190 = CARTESIAN_POINT('',(381.08,608.,928.)); +#3191 = VERTEX_POINT('',#3192); +#3192 = CARTESIAN_POINT('',(400.92,608.,928.)); +#3193 = LINE('',#3194,#3195); +#3194 = CARTESIAN_POINT('',(378.04,608.,928.)); +#3195 = VECTOR('',#3196,1.); +#3196 = DIRECTION('',(1.,0.,-0.)); +#3197 = ORIENTED_EDGE('',*,*,#3198,.T.); +#3198 = EDGE_CURVE('',#3189,#3199,#3201,.T.); +#3199 = VERTEX_POINT('',#3200); +#3200 = CARTESIAN_POINT('',(381.08,608.,912.)); +#3201 = LINE('',#3202,#3203); +#3202 = CARTESIAN_POINT('',(381.08,608.,481.)); +#3203 = VECTOR('',#3204,1.); +#3204 = DIRECTION('',(-0.,0.,-1.)); +#3205 = ORIENTED_EDGE('',*,*,#3206,.T.); +#3206 = EDGE_CURVE('',#3199,#3207,#3209,.T.); +#3207 = VERTEX_POINT('',#3208); +#3208 = CARTESIAN_POINT('',(400.92,608.,912.)); +#3209 = LINE('',#3210,#3211); +#3210 = CARTESIAN_POINT('',(378.04,608.,912.)); +#3211 = VECTOR('',#3212,1.); +#3212 = DIRECTION('',(1.,0.,-0.)); +#3213 = ORIENTED_EDGE('',*,*,#3214,.F.); +#3214 = EDGE_CURVE('',#3191,#3207,#3215,.T.); +#3215 = LINE('',#3216,#3217); +#3216 = CARTESIAN_POINT('',(400.92,608.,481.)); +#3217 = VECTOR('',#3218,1.); +#3218 = DIRECTION('',(-0.,0.,-1.)); +#3219 = PLANE('',#3220); +#3220 = AXIS2_PLACEMENT_3D('',#3221,#3222,#3223); +#3221 = CARTESIAN_POINT('',(375.,608.,50.)); +#3222 = DIRECTION('',(-0.,1.,0.)); +#3223 = DIRECTION('',(0.,0.,1.)); +#3224 = ADVANCED_FACE('',(#3225),#3236,.F.); +#3225 = FACE_BOUND('',#3226,.F.); +#3226 = EDGE_LOOP('',(#3227,#3228,#3229,#3235)); +#3227 = ORIENTED_EDGE('',*,*,#2439,.F.); +#3228 = ORIENTED_EDGE('',*,*,#2469,.T.); +#3229 = ORIENTED_EDGE('',*,*,#3230,.T.); +#3230 = EDGE_CURVE('',#2470,#2865,#3231,.T.); +#3231 = LINE('',#3232,#3233); +#3232 = CARTESIAN_POINT('',(407.,-608.,50.)); +#3233 = VECTOR('',#3234,1.); +#3234 = DIRECTION('',(-0.,1.,0.)); +#3235 = ORIENTED_EDGE('',*,*,#2864,.F.); +#3236 = PLANE('',#3237); +#3237 = AXIS2_PLACEMENT_3D('',#3238,#3239,#3240); +#3238 = CARTESIAN_POINT('',(375.,-608.,50.)); +#3239 = DIRECTION('',(0.,0.,1.)); +#3240 = DIRECTION('',(1.,0.,-0.)); +#3241 = ADVANCED_FACE('',(#3242),#3248,.T.); +#3242 = FACE_BOUND('',#3243,.T.); +#3243 = EDGE_LOOP('',(#3244,#3245,#3246,#3247)); +#3244 = ORIENTED_EDGE('',*,*,#2486,.F.); +#3245 = ORIENTED_EDGE('',*,*,#3230,.T.); +#3246 = ORIENTED_EDGE('',*,*,#2874,.T.); +#3247 = ORIENTED_EDGE('',*,*,#2842,.F.); +#3248 = PLANE('',#3249); +#3249 = AXIS2_PLACEMENT_3D('',#3250,#3251,#3252); +#3250 = CARTESIAN_POINT('',(407.,-608.,50.)); +#3251 = DIRECTION('',(1.,0.,-0.)); +#3252 = DIRECTION('',(0.,0.,1.)); +#3253 = ADVANCED_FACE('',(#3254),#3270,.F.); +#3254 = FACE_BOUND('',#3255,.F.); +#3255 = EDGE_LOOP('',(#3256,#3262,#3263,#3269)); +#3256 = ORIENTED_EDGE('',*,*,#3257,.F.); +#3257 = EDGE_CURVE('',#2495,#2883,#3258,.T.); +#3258 = LINE('',#3259,#3260); +#3259 = CARTESIAN_POINT('',(381.08,-611.,550.)); +#3260 = VECTOR('',#3261,1.); +#3261 = DIRECTION('',(-0.,1.,0.)); +#3262 = ORIENTED_EDGE('',*,*,#2494,.T.); +#3263 = ORIENTED_EDGE('',*,*,#3264,.T.); +#3264 = EDGE_CURVE('',#2497,#2885,#3265,.T.); +#3265 = LINE('',#3266,#3267); +#3266 = CARTESIAN_POINT('',(400.92,-611.,550.)); +#3267 = VECTOR('',#3268,1.); +#3268 = DIRECTION('',(-0.,1.,0.)); +#3269 = ORIENTED_EDGE('',*,*,#2882,.F.); +#3270 = PLANE('',#3271); +#3271 = AXIS2_PLACEMENT_3D('',#3272,#3273,#3274); +#3272 = CARTESIAN_POINT('',(381.08,-611.,550.)); +#3273 = DIRECTION('',(0.,0.,1.)); +#3274 = DIRECTION('',(1.,0.,-0.)); +#3275 = ADVANCED_FACE('',(#3276),#3287,.T.); +#3276 = FACE_BOUND('',#3277,.T.); +#3277 = EDGE_LOOP('',(#3278,#3279,#3280,#3286)); +#3278 = ORIENTED_EDGE('',*,*,#3257,.F.); +#3279 = ORIENTED_EDGE('',*,*,#2504,.T.); +#3280 = ORIENTED_EDGE('',*,*,#3281,.T.); +#3281 = EDGE_CURVE('',#2505,#2893,#3282,.T.); +#3282 = LINE('',#3283,#3284); +#3283 = CARTESIAN_POINT('',(381.08,-611.,534.)); +#3284 = VECTOR('',#3285,1.); +#3285 = DIRECTION('',(-0.,1.,0.)); +#3286 = ORIENTED_EDGE('',*,*,#2892,.F.); +#3287 = PLANE('',#3288); +#3288 = AXIS2_PLACEMENT_3D('',#3289,#3290,#3291); +#3289 = CARTESIAN_POINT('',(381.08,-611.,534.)); +#3290 = DIRECTION('',(1.,0.,-0.)); +#3291 = DIRECTION('',(0.,0.,1.)); +#3292 = ADVANCED_FACE('',(#3293),#3304,.F.); +#3293 = FACE_BOUND('',#3294,.F.); +#3294 = EDGE_LOOP('',(#3295,#3296,#3297,#3303)); +#3295 = ORIENTED_EDGE('',*,*,#3264,.F.); +#3296 = ORIENTED_EDGE('',*,*,#2520,.T.); +#3297 = ORIENTED_EDGE('',*,*,#3298,.T.); +#3298 = EDGE_CURVE('',#2513,#2901,#3299,.T.); +#3299 = LINE('',#3300,#3301); +#3300 = CARTESIAN_POINT('',(400.92,-611.,534.)); +#3301 = VECTOR('',#3302,1.); +#3302 = DIRECTION('',(-0.,1.,0.)); +#3303 = ORIENTED_EDGE('',*,*,#2908,.F.); +#3304 = PLANE('',#3305); +#3305 = AXIS2_PLACEMENT_3D('',#3306,#3307,#3308); +#3306 = CARTESIAN_POINT('',(400.92,-611.,534.)); +#3307 = DIRECTION('',(1.,0.,-0.)); +#3308 = DIRECTION('',(0.,0.,1.)); +#3309 = ADVANCED_FACE('',(#3310),#3316,.T.); +#3310 = FACE_BOUND('',#3311,.T.); +#3311 = EDGE_LOOP('',(#3312,#3313,#3314,#3315)); +#3312 = ORIENTED_EDGE('',*,*,#3281,.F.); +#3313 = ORIENTED_EDGE('',*,*,#2512,.T.); +#3314 = ORIENTED_EDGE('',*,*,#3298,.T.); +#3315 = ORIENTED_EDGE('',*,*,#2900,.F.); +#3316 = PLANE('',#3317); +#3317 = AXIS2_PLACEMENT_3D('',#3318,#3319,#3320); +#3318 = CARTESIAN_POINT('',(381.08,-611.,534.)); +#3319 = DIRECTION('',(0.,0.,1.)); +#3320 = DIRECTION('',(1.,0.,-0.)); +#3321 = ADVANCED_FACE('',(#3322),#3338,.F.); +#3322 = FACE_BOUND('',#3323,.F.); +#3323 = EDGE_LOOP('',(#3324,#3330,#3331,#3337)); +#3324 = ORIENTED_EDGE('',*,*,#3325,.F.); +#3325 = EDGE_CURVE('',#2529,#2917,#3326,.T.); +#3326 = LINE('',#3327,#3328); +#3327 = CARTESIAN_POINT('',(381.08,-611.,592.)); +#3328 = VECTOR('',#3329,1.); +#3329 = DIRECTION('',(-0.,1.,0.)); +#3330 = ORIENTED_EDGE('',*,*,#2528,.T.); +#3331 = ORIENTED_EDGE('',*,*,#3332,.T.); +#3332 = EDGE_CURVE('',#2531,#2919,#3333,.T.); +#3333 = LINE('',#3334,#3335); +#3334 = CARTESIAN_POINT('',(400.92,-611.,592.)); +#3335 = VECTOR('',#3336,1.); +#3336 = DIRECTION('',(-0.,1.,0.)); +#3337 = ORIENTED_EDGE('',*,*,#2916,.F.); +#3338 = PLANE('',#3339); +#3339 = AXIS2_PLACEMENT_3D('',#3340,#3341,#3342); +#3340 = CARTESIAN_POINT('',(381.08,-611.,592.)); +#3341 = DIRECTION('',(0.,0.,1.)); +#3342 = DIRECTION('',(1.,0.,-0.)); +#3343 = ADVANCED_FACE('',(#3344),#3355,.T.); +#3344 = FACE_BOUND('',#3345,.T.); +#3345 = EDGE_LOOP('',(#3346,#3347,#3348,#3354)); +#3346 = ORIENTED_EDGE('',*,*,#3325,.F.); +#3347 = ORIENTED_EDGE('',*,*,#2538,.T.); +#3348 = ORIENTED_EDGE('',*,*,#3349,.T.); +#3349 = EDGE_CURVE('',#2539,#2927,#3350,.T.); +#3350 = LINE('',#3351,#3352); +#3351 = CARTESIAN_POINT('',(381.08,-611.,576.)); +#3352 = VECTOR('',#3353,1.); +#3353 = DIRECTION('',(-0.,1.,0.)); +#3354 = ORIENTED_EDGE('',*,*,#2926,.F.); +#3355 = PLANE('',#3356); +#3356 = AXIS2_PLACEMENT_3D('',#3357,#3358,#3359); +#3357 = CARTESIAN_POINT('',(381.08,-611.,576.)); +#3358 = DIRECTION('',(1.,0.,-0.)); +#3359 = DIRECTION('',(0.,0.,1.)); +#3360 = ADVANCED_FACE('',(#3361),#3372,.F.); +#3361 = FACE_BOUND('',#3362,.F.); +#3362 = EDGE_LOOP('',(#3363,#3364,#3365,#3371)); +#3363 = ORIENTED_EDGE('',*,*,#3332,.F.); +#3364 = ORIENTED_EDGE('',*,*,#2554,.T.); +#3365 = ORIENTED_EDGE('',*,*,#3366,.T.); +#3366 = EDGE_CURVE('',#2547,#2935,#3367,.T.); +#3367 = LINE('',#3368,#3369); +#3368 = CARTESIAN_POINT('',(400.92,-611.,576.)); +#3369 = VECTOR('',#3370,1.); +#3370 = DIRECTION('',(-0.,1.,0.)); +#3371 = ORIENTED_EDGE('',*,*,#2942,.F.); +#3372 = PLANE('',#3373); +#3373 = AXIS2_PLACEMENT_3D('',#3374,#3375,#3376); +#3374 = CARTESIAN_POINT('',(400.92,-611.,576.)); +#3375 = DIRECTION('',(1.,0.,-0.)); +#3376 = DIRECTION('',(0.,0.,1.)); +#3377 = ADVANCED_FACE('',(#3378),#3384,.T.); +#3378 = FACE_BOUND('',#3379,.T.); +#3379 = EDGE_LOOP('',(#3380,#3381,#3382,#3383)); +#3380 = ORIENTED_EDGE('',*,*,#3349,.F.); +#3381 = ORIENTED_EDGE('',*,*,#2546,.T.); +#3382 = ORIENTED_EDGE('',*,*,#3366,.T.); +#3383 = ORIENTED_EDGE('',*,*,#2934,.F.); +#3384 = PLANE('',#3385); +#3385 = AXIS2_PLACEMENT_3D('',#3386,#3387,#3388); +#3386 = CARTESIAN_POINT('',(381.08,-611.,576.)); +#3387 = DIRECTION('',(0.,0.,1.)); +#3388 = DIRECTION('',(1.,0.,-0.)); +#3389 = ADVANCED_FACE('',(#3390),#3406,.F.); +#3390 = FACE_BOUND('',#3391,.F.); +#3391 = EDGE_LOOP('',(#3392,#3398,#3399,#3405)); +#3392 = ORIENTED_EDGE('',*,*,#3393,.F.); +#3393 = EDGE_CURVE('',#2563,#2951,#3394,.T.); +#3394 = LINE('',#3395,#3396); +#3395 = CARTESIAN_POINT('',(381.08,-611.,634.)); +#3396 = VECTOR('',#3397,1.); +#3397 = DIRECTION('',(-0.,1.,0.)); +#3398 = ORIENTED_EDGE('',*,*,#2562,.T.); +#3399 = ORIENTED_EDGE('',*,*,#3400,.T.); +#3400 = EDGE_CURVE('',#2565,#2953,#3401,.T.); +#3401 = LINE('',#3402,#3403); +#3402 = CARTESIAN_POINT('',(400.92,-611.,634.)); +#3403 = VECTOR('',#3404,1.); +#3404 = DIRECTION('',(-0.,1.,0.)); +#3405 = ORIENTED_EDGE('',*,*,#2950,.F.); +#3406 = PLANE('',#3407); +#3407 = AXIS2_PLACEMENT_3D('',#3408,#3409,#3410); +#3408 = CARTESIAN_POINT('',(381.08,-611.,634.)); +#3409 = DIRECTION('',(0.,0.,1.)); +#3410 = DIRECTION('',(1.,0.,-0.)); +#3411 = ADVANCED_FACE('',(#3412),#3423,.T.); +#3412 = FACE_BOUND('',#3413,.T.); +#3413 = EDGE_LOOP('',(#3414,#3415,#3416,#3422)); +#3414 = ORIENTED_EDGE('',*,*,#3393,.F.); +#3415 = ORIENTED_EDGE('',*,*,#2572,.T.); +#3416 = ORIENTED_EDGE('',*,*,#3417,.T.); +#3417 = EDGE_CURVE('',#2573,#2961,#3418,.T.); +#3418 = LINE('',#3419,#3420); +#3419 = CARTESIAN_POINT('',(381.08,-611.,618.)); +#3420 = VECTOR('',#3421,1.); +#3421 = DIRECTION('',(-0.,1.,0.)); +#3422 = ORIENTED_EDGE('',*,*,#2960,.F.); +#3423 = PLANE('',#3424); +#3424 = AXIS2_PLACEMENT_3D('',#3425,#3426,#3427); +#3425 = CARTESIAN_POINT('',(381.08,-611.,618.)); +#3426 = DIRECTION('',(1.,0.,-0.)); +#3427 = DIRECTION('',(0.,0.,1.)); +#3428 = ADVANCED_FACE('',(#3429),#3440,.F.); +#3429 = FACE_BOUND('',#3430,.F.); +#3430 = EDGE_LOOP('',(#3431,#3432,#3433,#3439)); +#3431 = ORIENTED_EDGE('',*,*,#3400,.F.); +#3432 = ORIENTED_EDGE('',*,*,#2588,.T.); +#3433 = ORIENTED_EDGE('',*,*,#3434,.T.); +#3434 = EDGE_CURVE('',#2581,#2969,#3435,.T.); +#3435 = LINE('',#3436,#3437); +#3436 = CARTESIAN_POINT('',(400.92,-611.,618.)); +#3437 = VECTOR('',#3438,1.); +#3438 = DIRECTION('',(-0.,1.,0.)); +#3439 = ORIENTED_EDGE('',*,*,#2976,.F.); +#3440 = PLANE('',#3441); +#3441 = AXIS2_PLACEMENT_3D('',#3442,#3443,#3444); +#3442 = CARTESIAN_POINT('',(400.92,-611.,618.)); +#3443 = DIRECTION('',(1.,0.,-0.)); +#3444 = DIRECTION('',(0.,0.,1.)); +#3445 = ADVANCED_FACE('',(#3446),#3452,.T.); +#3446 = FACE_BOUND('',#3447,.T.); +#3447 = EDGE_LOOP('',(#3448,#3449,#3450,#3451)); +#3448 = ORIENTED_EDGE('',*,*,#3417,.F.); +#3449 = ORIENTED_EDGE('',*,*,#2580,.T.); +#3450 = ORIENTED_EDGE('',*,*,#3434,.T.); +#3451 = ORIENTED_EDGE('',*,*,#2968,.F.); +#3452 = PLANE('',#3453); +#3453 = AXIS2_PLACEMENT_3D('',#3454,#3455,#3456); +#3454 = CARTESIAN_POINT('',(381.08,-611.,618.)); +#3455 = DIRECTION('',(0.,0.,1.)); +#3456 = DIRECTION('',(1.,0.,-0.)); +#3457 = ADVANCED_FACE('',(#3458),#3474,.F.); +#3458 = FACE_BOUND('',#3459,.F.); +#3459 = EDGE_LOOP('',(#3460,#3466,#3467,#3473)); +#3460 = ORIENTED_EDGE('',*,*,#3461,.F.); +#3461 = EDGE_CURVE('',#2597,#2985,#3462,.T.); +#3462 = LINE('',#3463,#3464); +#3463 = CARTESIAN_POINT('',(381.08,-611.,676.)); +#3464 = VECTOR('',#3465,1.); +#3465 = DIRECTION('',(-0.,1.,0.)); +#3466 = ORIENTED_EDGE('',*,*,#2596,.T.); +#3467 = ORIENTED_EDGE('',*,*,#3468,.T.); +#3468 = EDGE_CURVE('',#2599,#2987,#3469,.T.); +#3469 = LINE('',#3470,#3471); +#3470 = CARTESIAN_POINT('',(400.92,-611.,676.)); +#3471 = VECTOR('',#3472,1.); +#3472 = DIRECTION('',(-0.,1.,0.)); +#3473 = ORIENTED_EDGE('',*,*,#2984,.F.); +#3474 = PLANE('',#3475); +#3475 = AXIS2_PLACEMENT_3D('',#3476,#3477,#3478); +#3476 = CARTESIAN_POINT('',(381.08,-611.,676.)); +#3477 = DIRECTION('',(0.,0.,1.)); +#3478 = DIRECTION('',(1.,0.,-0.)); +#3479 = ADVANCED_FACE('',(#3480),#3491,.T.); +#3480 = FACE_BOUND('',#3481,.T.); +#3481 = EDGE_LOOP('',(#3482,#3483,#3484,#3490)); +#3482 = ORIENTED_EDGE('',*,*,#3461,.F.); +#3483 = ORIENTED_EDGE('',*,*,#2606,.T.); +#3484 = ORIENTED_EDGE('',*,*,#3485,.T.); +#3485 = EDGE_CURVE('',#2607,#2995,#3486,.T.); +#3486 = LINE('',#3487,#3488); +#3487 = CARTESIAN_POINT('',(381.08,-611.,660.)); +#3488 = VECTOR('',#3489,1.); +#3489 = DIRECTION('',(-0.,1.,0.)); +#3490 = ORIENTED_EDGE('',*,*,#2994,.F.); +#3491 = PLANE('',#3492); +#3492 = AXIS2_PLACEMENT_3D('',#3493,#3494,#3495); +#3493 = CARTESIAN_POINT('',(381.08,-611.,660.)); +#3494 = DIRECTION('',(1.,0.,-0.)); +#3495 = DIRECTION('',(0.,0.,1.)); +#3496 = ADVANCED_FACE('',(#3497),#3508,.F.); +#3497 = FACE_BOUND('',#3498,.F.); +#3498 = EDGE_LOOP('',(#3499,#3500,#3501,#3507)); +#3499 = ORIENTED_EDGE('',*,*,#3468,.F.); +#3500 = ORIENTED_EDGE('',*,*,#2622,.T.); +#3501 = ORIENTED_EDGE('',*,*,#3502,.T.); +#3502 = EDGE_CURVE('',#2615,#3003,#3503,.T.); +#3503 = LINE('',#3504,#3505); +#3504 = CARTESIAN_POINT('',(400.92,-611.,660.)); +#3505 = VECTOR('',#3506,1.); +#3506 = DIRECTION('',(-0.,1.,0.)); +#3507 = ORIENTED_EDGE('',*,*,#3010,.F.); +#3508 = PLANE('',#3509); +#3509 = AXIS2_PLACEMENT_3D('',#3510,#3511,#3512); +#3510 = CARTESIAN_POINT('',(400.92,-611.,660.)); +#3511 = DIRECTION('',(1.,0.,-0.)); +#3512 = DIRECTION('',(0.,0.,1.)); +#3513 = ADVANCED_FACE('',(#3514),#3520,.T.); +#3514 = FACE_BOUND('',#3515,.T.); +#3515 = EDGE_LOOP('',(#3516,#3517,#3518,#3519)); +#3516 = ORIENTED_EDGE('',*,*,#3485,.F.); +#3517 = ORIENTED_EDGE('',*,*,#2614,.T.); +#3518 = ORIENTED_EDGE('',*,*,#3502,.T.); +#3519 = ORIENTED_EDGE('',*,*,#3002,.F.); +#3520 = PLANE('',#3521); +#3521 = AXIS2_PLACEMENT_3D('',#3522,#3523,#3524); +#3522 = CARTESIAN_POINT('',(381.08,-611.,660.)); +#3523 = DIRECTION('',(0.,0.,1.)); +#3524 = DIRECTION('',(1.,0.,-0.)); +#3525 = ADVANCED_FACE('',(#3526),#3542,.F.); +#3526 = FACE_BOUND('',#3527,.F.); +#3527 = EDGE_LOOP('',(#3528,#3534,#3535,#3541)); +#3528 = ORIENTED_EDGE('',*,*,#3529,.F.); +#3529 = EDGE_CURVE('',#2631,#3019,#3530,.T.); +#3530 = LINE('',#3531,#3532); +#3531 = CARTESIAN_POINT('',(381.08,-611.,718.)); +#3532 = VECTOR('',#3533,1.); +#3533 = DIRECTION('',(-0.,1.,0.)); +#3534 = ORIENTED_EDGE('',*,*,#2630,.T.); +#3535 = ORIENTED_EDGE('',*,*,#3536,.T.); +#3536 = EDGE_CURVE('',#2633,#3021,#3537,.T.); +#3537 = LINE('',#3538,#3539); +#3538 = CARTESIAN_POINT('',(400.92,-611.,718.)); +#3539 = VECTOR('',#3540,1.); +#3540 = DIRECTION('',(-0.,1.,0.)); +#3541 = ORIENTED_EDGE('',*,*,#3018,.F.); +#3542 = PLANE('',#3543); +#3543 = AXIS2_PLACEMENT_3D('',#3544,#3545,#3546); +#3544 = CARTESIAN_POINT('',(381.08,-611.,718.)); +#3545 = DIRECTION('',(0.,0.,1.)); +#3546 = DIRECTION('',(1.,0.,-0.)); +#3547 = ADVANCED_FACE('',(#3548),#3559,.T.); +#3548 = FACE_BOUND('',#3549,.T.); +#3549 = EDGE_LOOP('',(#3550,#3551,#3552,#3558)); +#3550 = ORIENTED_EDGE('',*,*,#3529,.F.); +#3551 = ORIENTED_EDGE('',*,*,#2640,.T.); +#3552 = ORIENTED_EDGE('',*,*,#3553,.T.); +#3553 = EDGE_CURVE('',#2641,#3029,#3554,.T.); +#3554 = LINE('',#3555,#3556); +#3555 = CARTESIAN_POINT('',(381.08,-611.,702.)); +#3556 = VECTOR('',#3557,1.); +#3557 = DIRECTION('',(-0.,1.,0.)); +#3558 = ORIENTED_EDGE('',*,*,#3028,.F.); +#3559 = PLANE('',#3560); +#3560 = AXIS2_PLACEMENT_3D('',#3561,#3562,#3563); +#3561 = CARTESIAN_POINT('',(381.08,-611.,702.)); +#3562 = DIRECTION('',(1.,0.,-0.)); +#3563 = DIRECTION('',(0.,0.,1.)); +#3564 = ADVANCED_FACE('',(#3565),#3576,.F.); +#3565 = FACE_BOUND('',#3566,.F.); +#3566 = EDGE_LOOP('',(#3567,#3568,#3569,#3575)); +#3567 = ORIENTED_EDGE('',*,*,#3536,.F.); +#3568 = ORIENTED_EDGE('',*,*,#2656,.T.); +#3569 = ORIENTED_EDGE('',*,*,#3570,.T.); +#3570 = EDGE_CURVE('',#2649,#3037,#3571,.T.); +#3571 = LINE('',#3572,#3573); +#3572 = CARTESIAN_POINT('',(400.92,-611.,702.)); +#3573 = VECTOR('',#3574,1.); +#3574 = DIRECTION('',(-0.,1.,0.)); +#3575 = ORIENTED_EDGE('',*,*,#3044,.F.); +#3576 = PLANE('',#3577); +#3577 = AXIS2_PLACEMENT_3D('',#3578,#3579,#3580); +#3578 = CARTESIAN_POINT('',(400.92,-611.,702.)); +#3579 = DIRECTION('',(1.,0.,-0.)); +#3580 = DIRECTION('',(0.,0.,1.)); +#3581 = ADVANCED_FACE('',(#3582),#3588,.T.); +#3582 = FACE_BOUND('',#3583,.T.); +#3583 = EDGE_LOOP('',(#3584,#3585,#3586,#3587)); +#3584 = ORIENTED_EDGE('',*,*,#3553,.F.); +#3585 = ORIENTED_EDGE('',*,*,#2648,.T.); +#3586 = ORIENTED_EDGE('',*,*,#3570,.T.); +#3587 = ORIENTED_EDGE('',*,*,#3036,.F.); +#3588 = PLANE('',#3589); +#3589 = AXIS2_PLACEMENT_3D('',#3590,#3591,#3592); +#3590 = CARTESIAN_POINT('',(381.08,-611.,702.)); +#3591 = DIRECTION('',(0.,0.,1.)); +#3592 = DIRECTION('',(1.,0.,-0.)); +#3593 = ADVANCED_FACE('',(#3594),#3610,.F.); +#3594 = FACE_BOUND('',#3595,.F.); +#3595 = EDGE_LOOP('',(#3596,#3602,#3603,#3609)); +#3596 = ORIENTED_EDGE('',*,*,#3597,.F.); +#3597 = EDGE_CURVE('',#2665,#3053,#3598,.T.); +#3598 = LINE('',#3599,#3600); +#3599 = CARTESIAN_POINT('',(381.08,-611.,760.)); +#3600 = VECTOR('',#3601,1.); +#3601 = DIRECTION('',(-0.,1.,0.)); +#3602 = ORIENTED_EDGE('',*,*,#2664,.T.); +#3603 = ORIENTED_EDGE('',*,*,#3604,.T.); +#3604 = EDGE_CURVE('',#2667,#3055,#3605,.T.); +#3605 = LINE('',#3606,#3607); +#3606 = CARTESIAN_POINT('',(400.92,-611.,760.)); +#3607 = VECTOR('',#3608,1.); +#3608 = DIRECTION('',(-0.,1.,0.)); +#3609 = ORIENTED_EDGE('',*,*,#3052,.F.); +#3610 = PLANE('',#3611); +#3611 = AXIS2_PLACEMENT_3D('',#3612,#3613,#3614); +#3612 = CARTESIAN_POINT('',(381.08,-611.,760.)); +#3613 = DIRECTION('',(0.,0.,1.)); +#3614 = DIRECTION('',(1.,0.,-0.)); +#3615 = ADVANCED_FACE('',(#3616),#3627,.T.); +#3616 = FACE_BOUND('',#3617,.T.); +#3617 = EDGE_LOOP('',(#3618,#3619,#3620,#3626)); +#3618 = ORIENTED_EDGE('',*,*,#3597,.F.); +#3619 = ORIENTED_EDGE('',*,*,#2674,.T.); +#3620 = ORIENTED_EDGE('',*,*,#3621,.T.); +#3621 = EDGE_CURVE('',#2675,#3063,#3622,.T.); +#3622 = LINE('',#3623,#3624); +#3623 = CARTESIAN_POINT('',(381.08,-611.,744.)); +#3624 = VECTOR('',#3625,1.); +#3625 = DIRECTION('',(-0.,1.,0.)); +#3626 = ORIENTED_EDGE('',*,*,#3062,.F.); +#3627 = PLANE('',#3628); +#3628 = AXIS2_PLACEMENT_3D('',#3629,#3630,#3631); +#3629 = CARTESIAN_POINT('',(381.08,-611.,744.)); +#3630 = DIRECTION('',(1.,0.,-0.)); +#3631 = DIRECTION('',(0.,0.,1.)); +#3632 = ADVANCED_FACE('',(#3633),#3644,.F.); +#3633 = FACE_BOUND('',#3634,.F.); +#3634 = EDGE_LOOP('',(#3635,#3636,#3637,#3643)); +#3635 = ORIENTED_EDGE('',*,*,#3604,.F.); +#3636 = ORIENTED_EDGE('',*,*,#2690,.T.); +#3637 = ORIENTED_EDGE('',*,*,#3638,.T.); +#3638 = EDGE_CURVE('',#2683,#3071,#3639,.T.); +#3639 = LINE('',#3640,#3641); +#3640 = CARTESIAN_POINT('',(400.92,-611.,744.)); +#3641 = VECTOR('',#3642,1.); +#3642 = DIRECTION('',(-0.,1.,0.)); +#3643 = ORIENTED_EDGE('',*,*,#3078,.F.); +#3644 = PLANE('',#3645); +#3645 = AXIS2_PLACEMENT_3D('',#3646,#3647,#3648); +#3646 = CARTESIAN_POINT('',(400.92,-611.,744.)); +#3647 = DIRECTION('',(1.,0.,-0.)); +#3648 = DIRECTION('',(0.,0.,1.)); +#3649 = ADVANCED_FACE('',(#3650),#3656,.T.); +#3650 = FACE_BOUND('',#3651,.T.); +#3651 = EDGE_LOOP('',(#3652,#3653,#3654,#3655)); +#3652 = ORIENTED_EDGE('',*,*,#3621,.F.); +#3653 = ORIENTED_EDGE('',*,*,#2682,.T.); +#3654 = ORIENTED_EDGE('',*,*,#3638,.T.); +#3655 = ORIENTED_EDGE('',*,*,#3070,.F.); +#3656 = PLANE('',#3657); +#3657 = AXIS2_PLACEMENT_3D('',#3658,#3659,#3660); +#3658 = CARTESIAN_POINT('',(381.08,-611.,744.)); +#3659 = DIRECTION('',(0.,0.,1.)); +#3660 = DIRECTION('',(1.,0.,-0.)); +#3661 = ADVANCED_FACE('',(#3662),#3678,.F.); +#3662 = FACE_BOUND('',#3663,.F.); +#3663 = EDGE_LOOP('',(#3664,#3670,#3671,#3677)); +#3664 = ORIENTED_EDGE('',*,*,#3665,.F.); +#3665 = EDGE_CURVE('',#2699,#3087,#3666,.T.); +#3666 = LINE('',#3667,#3668); +#3667 = CARTESIAN_POINT('',(381.08,-611.,802.)); +#3668 = VECTOR('',#3669,1.); +#3669 = DIRECTION('',(-0.,1.,0.)); +#3670 = ORIENTED_EDGE('',*,*,#2698,.T.); +#3671 = ORIENTED_EDGE('',*,*,#3672,.T.); +#3672 = EDGE_CURVE('',#2701,#3089,#3673,.T.); +#3673 = LINE('',#3674,#3675); +#3674 = CARTESIAN_POINT('',(400.92,-611.,802.)); +#3675 = VECTOR('',#3676,1.); +#3676 = DIRECTION('',(-0.,1.,0.)); +#3677 = ORIENTED_EDGE('',*,*,#3086,.F.); +#3678 = PLANE('',#3679); +#3679 = AXIS2_PLACEMENT_3D('',#3680,#3681,#3682); +#3680 = CARTESIAN_POINT('',(381.08,-611.,802.)); +#3681 = DIRECTION('',(0.,0.,1.)); +#3682 = DIRECTION('',(1.,0.,-0.)); +#3683 = ADVANCED_FACE('',(#3684),#3695,.T.); +#3684 = FACE_BOUND('',#3685,.T.); +#3685 = EDGE_LOOP('',(#3686,#3687,#3688,#3694)); +#3686 = ORIENTED_EDGE('',*,*,#3665,.F.); +#3687 = ORIENTED_EDGE('',*,*,#2708,.T.); +#3688 = ORIENTED_EDGE('',*,*,#3689,.T.); +#3689 = EDGE_CURVE('',#2709,#3097,#3690,.T.); +#3690 = LINE('',#3691,#3692); +#3691 = CARTESIAN_POINT('',(381.08,-611.,786.)); +#3692 = VECTOR('',#3693,1.); +#3693 = DIRECTION('',(-0.,1.,0.)); +#3694 = ORIENTED_EDGE('',*,*,#3096,.F.); +#3695 = PLANE('',#3696); +#3696 = AXIS2_PLACEMENT_3D('',#3697,#3698,#3699); +#3697 = CARTESIAN_POINT('',(381.08,-611.,786.)); +#3698 = DIRECTION('',(1.,0.,-0.)); +#3699 = DIRECTION('',(0.,0.,1.)); +#3700 = ADVANCED_FACE('',(#3701),#3712,.F.); +#3701 = FACE_BOUND('',#3702,.F.); +#3702 = EDGE_LOOP('',(#3703,#3704,#3705,#3711)); +#3703 = ORIENTED_EDGE('',*,*,#3672,.F.); +#3704 = ORIENTED_EDGE('',*,*,#2724,.T.); +#3705 = ORIENTED_EDGE('',*,*,#3706,.T.); +#3706 = EDGE_CURVE('',#2717,#3105,#3707,.T.); +#3707 = LINE('',#3708,#3709); +#3708 = CARTESIAN_POINT('',(400.92,-611.,786.)); +#3709 = VECTOR('',#3710,1.); +#3710 = DIRECTION('',(-0.,1.,0.)); +#3711 = ORIENTED_EDGE('',*,*,#3112,.F.); +#3712 = PLANE('',#3713); +#3713 = AXIS2_PLACEMENT_3D('',#3714,#3715,#3716); +#3714 = CARTESIAN_POINT('',(400.92,-611.,786.)); +#3715 = DIRECTION('',(1.,0.,-0.)); +#3716 = DIRECTION('',(0.,0.,1.)); +#3717 = ADVANCED_FACE('',(#3718),#3724,.T.); +#3718 = FACE_BOUND('',#3719,.T.); +#3719 = EDGE_LOOP('',(#3720,#3721,#3722,#3723)); +#3720 = ORIENTED_EDGE('',*,*,#3689,.F.); +#3721 = ORIENTED_EDGE('',*,*,#2716,.T.); +#3722 = ORIENTED_EDGE('',*,*,#3706,.T.); +#3723 = ORIENTED_EDGE('',*,*,#3104,.F.); +#3724 = PLANE('',#3725); +#3725 = AXIS2_PLACEMENT_3D('',#3726,#3727,#3728); +#3726 = CARTESIAN_POINT('',(381.08,-611.,786.)); +#3727 = DIRECTION('',(0.,0.,1.)); +#3728 = DIRECTION('',(1.,0.,-0.)); +#3729 = ADVANCED_FACE('',(#3730),#3746,.F.); +#3730 = FACE_BOUND('',#3731,.F.); +#3731 = EDGE_LOOP('',(#3732,#3738,#3739,#3745)); +#3732 = ORIENTED_EDGE('',*,*,#3733,.F.); +#3733 = EDGE_CURVE('',#2733,#3121,#3734,.T.); +#3734 = LINE('',#3735,#3736); +#3735 = CARTESIAN_POINT('',(381.08,-611.,844.)); +#3736 = VECTOR('',#3737,1.); +#3737 = DIRECTION('',(-0.,1.,0.)); +#3738 = ORIENTED_EDGE('',*,*,#2732,.T.); +#3739 = ORIENTED_EDGE('',*,*,#3740,.T.); +#3740 = EDGE_CURVE('',#2735,#3123,#3741,.T.); +#3741 = LINE('',#3742,#3743); +#3742 = CARTESIAN_POINT('',(400.92,-611.,844.)); +#3743 = VECTOR('',#3744,1.); +#3744 = DIRECTION('',(-0.,1.,0.)); +#3745 = ORIENTED_EDGE('',*,*,#3120,.F.); +#3746 = PLANE('',#3747); +#3747 = AXIS2_PLACEMENT_3D('',#3748,#3749,#3750); +#3748 = CARTESIAN_POINT('',(381.08,-611.,844.)); +#3749 = DIRECTION('',(0.,0.,1.)); +#3750 = DIRECTION('',(1.,0.,-0.)); +#3751 = ADVANCED_FACE('',(#3752),#3763,.T.); +#3752 = FACE_BOUND('',#3753,.T.); +#3753 = EDGE_LOOP('',(#3754,#3755,#3756,#3762)); +#3754 = ORIENTED_EDGE('',*,*,#3733,.F.); +#3755 = ORIENTED_EDGE('',*,*,#2742,.T.); +#3756 = ORIENTED_EDGE('',*,*,#3757,.T.); +#3757 = EDGE_CURVE('',#2743,#3131,#3758,.T.); +#3758 = LINE('',#3759,#3760); +#3759 = CARTESIAN_POINT('',(381.08,-611.,828.)); +#3760 = VECTOR('',#3761,1.); +#3761 = DIRECTION('',(-0.,1.,0.)); +#3762 = ORIENTED_EDGE('',*,*,#3130,.F.); +#3763 = PLANE('',#3764); +#3764 = AXIS2_PLACEMENT_3D('',#3765,#3766,#3767); +#3765 = CARTESIAN_POINT('',(381.08,-611.,828.)); +#3766 = DIRECTION('',(1.,0.,-0.)); +#3767 = DIRECTION('',(0.,0.,1.)); +#3768 = ADVANCED_FACE('',(#3769),#3780,.F.); +#3769 = FACE_BOUND('',#3770,.F.); +#3770 = EDGE_LOOP('',(#3771,#3772,#3773,#3779)); +#3771 = ORIENTED_EDGE('',*,*,#3740,.F.); +#3772 = ORIENTED_EDGE('',*,*,#2758,.T.); +#3773 = ORIENTED_EDGE('',*,*,#3774,.T.); +#3774 = EDGE_CURVE('',#2751,#3139,#3775,.T.); +#3775 = LINE('',#3776,#3777); +#3776 = CARTESIAN_POINT('',(400.92,-611.,828.)); +#3777 = VECTOR('',#3778,1.); +#3778 = DIRECTION('',(-0.,1.,0.)); +#3779 = ORIENTED_EDGE('',*,*,#3146,.F.); +#3780 = PLANE('',#3781); +#3781 = AXIS2_PLACEMENT_3D('',#3782,#3783,#3784); +#3782 = CARTESIAN_POINT('',(400.92,-611.,828.)); +#3783 = DIRECTION('',(1.,0.,-0.)); +#3784 = DIRECTION('',(0.,0.,1.)); +#3785 = ADVANCED_FACE('',(#3786),#3792,.T.); +#3786 = FACE_BOUND('',#3787,.T.); +#3787 = EDGE_LOOP('',(#3788,#3789,#3790,#3791)); +#3788 = ORIENTED_EDGE('',*,*,#3757,.F.); +#3789 = ORIENTED_EDGE('',*,*,#2750,.T.); +#3790 = ORIENTED_EDGE('',*,*,#3774,.T.); +#3791 = ORIENTED_EDGE('',*,*,#3138,.F.); +#3792 = PLANE('',#3793); +#3793 = AXIS2_PLACEMENT_3D('',#3794,#3795,#3796); +#3794 = CARTESIAN_POINT('',(381.08,-611.,828.)); +#3795 = DIRECTION('',(0.,0.,1.)); +#3796 = DIRECTION('',(1.,0.,-0.)); +#3797 = ADVANCED_FACE('',(#3798),#3814,.F.); +#3798 = FACE_BOUND('',#3799,.F.); +#3799 = EDGE_LOOP('',(#3800,#3806,#3807,#3813)); +#3800 = ORIENTED_EDGE('',*,*,#3801,.F.); +#3801 = EDGE_CURVE('',#2767,#3155,#3802,.T.); +#3802 = LINE('',#3803,#3804); +#3803 = CARTESIAN_POINT('',(381.08,-611.,886.)); +#3804 = VECTOR('',#3805,1.); +#3805 = DIRECTION('',(-0.,1.,0.)); +#3806 = ORIENTED_EDGE('',*,*,#2766,.T.); +#3807 = ORIENTED_EDGE('',*,*,#3808,.T.); +#3808 = EDGE_CURVE('',#2769,#3157,#3809,.T.); +#3809 = LINE('',#3810,#3811); +#3810 = CARTESIAN_POINT('',(400.92,-611.,886.)); +#3811 = VECTOR('',#3812,1.); +#3812 = DIRECTION('',(-0.,1.,0.)); +#3813 = ORIENTED_EDGE('',*,*,#3154,.F.); +#3814 = PLANE('',#3815); +#3815 = AXIS2_PLACEMENT_3D('',#3816,#3817,#3818); +#3816 = CARTESIAN_POINT('',(381.08,-611.,886.)); +#3817 = DIRECTION('',(0.,0.,1.)); +#3818 = DIRECTION('',(1.,0.,-0.)); +#3819 = ADVANCED_FACE('',(#3820),#3831,.T.); +#3820 = FACE_BOUND('',#3821,.T.); +#3821 = EDGE_LOOP('',(#3822,#3823,#3824,#3830)); +#3822 = ORIENTED_EDGE('',*,*,#3801,.F.); +#3823 = ORIENTED_EDGE('',*,*,#2776,.T.); +#3824 = ORIENTED_EDGE('',*,*,#3825,.T.); +#3825 = EDGE_CURVE('',#2777,#3165,#3826,.T.); +#3826 = LINE('',#3827,#3828); +#3827 = CARTESIAN_POINT('',(381.08,-611.,870.)); +#3828 = VECTOR('',#3829,1.); +#3829 = DIRECTION('',(-0.,1.,0.)); +#3830 = ORIENTED_EDGE('',*,*,#3164,.F.); +#3831 = PLANE('',#3832); +#3832 = AXIS2_PLACEMENT_3D('',#3833,#3834,#3835); +#3833 = CARTESIAN_POINT('',(381.08,-611.,870.)); +#3834 = DIRECTION('',(1.,0.,-0.)); +#3835 = DIRECTION('',(0.,0.,1.)); +#3836 = ADVANCED_FACE('',(#3837),#3848,.F.); +#3837 = FACE_BOUND('',#3838,.F.); +#3838 = EDGE_LOOP('',(#3839,#3840,#3841,#3847)); +#3839 = ORIENTED_EDGE('',*,*,#3808,.F.); +#3840 = ORIENTED_EDGE('',*,*,#2792,.T.); +#3841 = ORIENTED_EDGE('',*,*,#3842,.T.); +#3842 = EDGE_CURVE('',#2785,#3173,#3843,.T.); +#3843 = LINE('',#3844,#3845); +#3844 = CARTESIAN_POINT('',(400.92,-611.,870.)); +#3845 = VECTOR('',#3846,1.); +#3846 = DIRECTION('',(-0.,1.,0.)); +#3847 = ORIENTED_EDGE('',*,*,#3180,.F.); +#3848 = PLANE('',#3849); +#3849 = AXIS2_PLACEMENT_3D('',#3850,#3851,#3852); +#3850 = CARTESIAN_POINT('',(400.92,-611.,870.)); +#3851 = DIRECTION('',(1.,0.,-0.)); +#3852 = DIRECTION('',(0.,0.,1.)); +#3853 = ADVANCED_FACE('',(#3854),#3860,.T.); +#3854 = FACE_BOUND('',#3855,.T.); +#3855 = EDGE_LOOP('',(#3856,#3857,#3858,#3859)); +#3856 = ORIENTED_EDGE('',*,*,#3825,.F.); +#3857 = ORIENTED_EDGE('',*,*,#2784,.T.); +#3858 = ORIENTED_EDGE('',*,*,#3842,.T.); +#3859 = ORIENTED_EDGE('',*,*,#3172,.F.); +#3860 = PLANE('',#3861); +#3861 = AXIS2_PLACEMENT_3D('',#3862,#3863,#3864); +#3862 = CARTESIAN_POINT('',(381.08,-611.,870.)); +#3863 = DIRECTION('',(0.,0.,1.)); +#3864 = DIRECTION('',(1.,0.,-0.)); +#3865 = ADVANCED_FACE('',(#3866),#3882,.F.); +#3866 = FACE_BOUND('',#3867,.F.); +#3867 = EDGE_LOOP('',(#3868,#3874,#3875,#3881)); +#3868 = ORIENTED_EDGE('',*,*,#3869,.F.); +#3869 = EDGE_CURVE('',#2801,#3189,#3870,.T.); +#3870 = LINE('',#3871,#3872); +#3871 = CARTESIAN_POINT('',(381.08,-611.,928.)); +#3872 = VECTOR('',#3873,1.); +#3873 = DIRECTION('',(-0.,1.,0.)); +#3874 = ORIENTED_EDGE('',*,*,#2800,.T.); +#3875 = ORIENTED_EDGE('',*,*,#3876,.T.); +#3876 = EDGE_CURVE('',#2803,#3191,#3877,.T.); +#3877 = LINE('',#3878,#3879); +#3878 = CARTESIAN_POINT('',(400.92,-611.,928.)); +#3879 = VECTOR('',#3880,1.); +#3880 = DIRECTION('',(-0.,1.,0.)); +#3881 = ORIENTED_EDGE('',*,*,#3188,.F.); +#3882 = PLANE('',#3883); +#3883 = AXIS2_PLACEMENT_3D('',#3884,#3885,#3886); +#3884 = CARTESIAN_POINT('',(381.08,-611.,928.)); +#3885 = DIRECTION('',(0.,0.,1.)); +#3886 = DIRECTION('',(1.,0.,-0.)); +#3887 = ADVANCED_FACE('',(#3888),#3899,.F.); +#3888 = FACE_BOUND('',#3889,.F.); +#3889 = EDGE_LOOP('',(#3890,#3891,#3892,#3898)); +#3890 = ORIENTED_EDGE('',*,*,#3876,.F.); +#3891 = ORIENTED_EDGE('',*,*,#2826,.T.); +#3892 = ORIENTED_EDGE('',*,*,#3893,.T.); +#3893 = EDGE_CURVE('',#2819,#3207,#3894,.T.); +#3894 = LINE('',#3895,#3896); +#3895 = CARTESIAN_POINT('',(400.92,-611.,912.)); +#3896 = VECTOR('',#3897,1.); +#3897 = DIRECTION('',(-0.,1.,0.)); +#3898 = ORIENTED_EDGE('',*,*,#3214,.F.); +#3899 = PLANE('',#3900); +#3900 = AXIS2_PLACEMENT_3D('',#3901,#3902,#3903); +#3901 = CARTESIAN_POINT('',(400.92,-611.,912.)); +#3902 = DIRECTION('',(1.,0.,-0.)); +#3903 = DIRECTION('',(0.,0.,1.)); +#3904 = ADVANCED_FACE('',(#3905),#3916,.T.); +#3905 = FACE_BOUND('',#3906,.T.); +#3906 = EDGE_LOOP('',(#3907,#3913,#3914,#3915)); +#3907 = ORIENTED_EDGE('',*,*,#3908,.F.); +#3908 = EDGE_CURVE('',#2811,#3199,#3909,.T.); +#3909 = LINE('',#3910,#3911); +#3910 = CARTESIAN_POINT('',(381.08,-611.,912.)); +#3911 = VECTOR('',#3912,1.); +#3912 = DIRECTION('',(-0.,1.,0.)); +#3913 = ORIENTED_EDGE('',*,*,#2818,.T.); +#3914 = ORIENTED_EDGE('',*,*,#3893,.T.); +#3915 = ORIENTED_EDGE('',*,*,#3206,.F.); +#3916 = PLANE('',#3917); +#3917 = AXIS2_PLACEMENT_3D('',#3918,#3919,#3920); +#3918 = CARTESIAN_POINT('',(381.08,-611.,912.)); +#3919 = DIRECTION('',(0.,0.,1.)); +#3920 = DIRECTION('',(1.,0.,-0.)); +#3921 = ADVANCED_FACE('',(#3922),#3928,.T.); +#3922 = FACE_BOUND('',#3923,.T.); +#3923 = EDGE_LOOP('',(#3924,#3925,#3926,#3927)); +#3924 = ORIENTED_EDGE('',*,*,#3869,.F.); +#3925 = ORIENTED_EDGE('',*,*,#2810,.T.); +#3926 = ORIENTED_EDGE('',*,*,#3908,.T.); +#3927 = ORIENTED_EDGE('',*,*,#3198,.F.); +#3928 = PLANE('',#3929); +#3929 = AXIS2_PLACEMENT_3D('',#3930,#3931,#3932); +#3930 = CARTESIAN_POINT('',(381.08,-611.,912.)); +#3931 = DIRECTION('',(1.,0.,-0.)); +#3932 = DIRECTION('',(0.,0.,1.)); +#3933 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3937)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#3934,#3935,#3936)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#3934 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#3935 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#3936 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#3937 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-07),#3934, + 'distance_accuracy_value','confusion accuracy'); +#3938 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3939,#3941); +#3939 = ( REPRESENTATION_RELATIONSHIP('','',#2422,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3940) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#3940 = ITEM_DEFINED_TRANSFORMATION('','',#11,#23); +#3941 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #3942); +#3942 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('3','NAU03_Right_Side_Panel','', + #5,#2417,$); +#3943 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2419)); +#3944 = SHAPE_DEFINITION_REPRESENTATION(#3945,#3951); +#3945 = PRODUCT_DEFINITION_SHAPE('','',#3946); +#3946 = PRODUCT_DEFINITION('design','',#3947,#3950); +#3947 = PRODUCT_DEFINITION_FORMATION('','',#3948); +#3948 = PRODUCT('NAU03_Rear_Panel','NAU03_Rear_Panel','',(#3949)); +#3949 = PRODUCT_CONTEXT('',#2,'mechanical'); +#3950 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#3951 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3952),#4102); +#3952 = MANIFOLD_SOLID_BREP('',#3953); +#3953 = CLOSED_SHELL('',(#3954,#3994,#4034,#4056,#4078,#4090)); +#3954 = ADVANCED_FACE('',(#3955),#3989,.F.); +#3955 = FACE_BOUND('',#3956,.F.); +#3956 = EDGE_LOOP('',(#3957,#3967,#3975,#3983)); +#3957 = ORIENTED_EDGE('',*,*,#3958,.F.); +#3958 = EDGE_CURVE('',#3959,#3961,#3963,.T.); +#3959 = VERTEX_POINT('',#3960); +#3960 = CARTESIAN_POINT('',(-357.,650.,45.)); +#3961 = VERTEX_POINT('',#3962); +#3962 = CARTESIAN_POINT('',(-357.,650.,2.255E+03)); +#3963 = LINE('',#3964,#3965); +#3964 = CARTESIAN_POINT('',(-357.,650.,45.)); +#3965 = VECTOR('',#3966,1.); +#3966 = DIRECTION('',(0.,0.,1.)); +#3967 = ORIENTED_EDGE('',*,*,#3968,.T.); +#3968 = EDGE_CURVE('',#3959,#3969,#3971,.T.); +#3969 = VERTEX_POINT('',#3970); +#3970 = CARTESIAN_POINT('',(-357.,682.,45.)); +#3971 = LINE('',#3972,#3973); +#3972 = CARTESIAN_POINT('',(-357.,650.,45.)); +#3973 = VECTOR('',#3974,1.); +#3974 = DIRECTION('',(-0.,1.,0.)); +#3975 = ORIENTED_EDGE('',*,*,#3976,.T.); +#3976 = EDGE_CURVE('',#3969,#3977,#3979,.T.); +#3977 = VERTEX_POINT('',#3978); +#3978 = CARTESIAN_POINT('',(-357.,682.,2.255E+03)); +#3979 = LINE('',#3980,#3981); +#3980 = CARTESIAN_POINT('',(-357.,682.,45.)); +#3981 = VECTOR('',#3982,1.); +#3982 = DIRECTION('',(0.,0.,1.)); +#3983 = ORIENTED_EDGE('',*,*,#3984,.F.); +#3984 = EDGE_CURVE('',#3961,#3977,#3985,.T.); +#3985 = LINE('',#3986,#3987); +#3986 = CARTESIAN_POINT('',(-357.,650.,2.255E+03)); +#3987 = VECTOR('',#3988,1.); +#3988 = DIRECTION('',(-0.,1.,0.)); +#3989 = PLANE('',#3990); +#3990 = AXIS2_PLACEMENT_3D('',#3991,#3992,#3993); +#3991 = CARTESIAN_POINT('',(-357.,650.,45.)); +#3992 = DIRECTION('',(1.,0.,-0.)); +#3993 = DIRECTION('',(0.,0.,1.)); +#3994 = ADVANCED_FACE('',(#3995),#4029,.T.); +#3995 = FACE_BOUND('',#3996,.T.); +#3996 = EDGE_LOOP('',(#3997,#4007,#4015,#4023)); +#3997 = ORIENTED_EDGE('',*,*,#3998,.F.); +#3998 = EDGE_CURVE('',#3999,#4001,#4003,.T.); +#3999 = VERTEX_POINT('',#4000); +#4000 = CARTESIAN_POINT('',(357.,650.,45.)); +#4001 = VERTEX_POINT('',#4002); +#4002 = CARTESIAN_POINT('',(357.,650.,2.255E+03)); +#4003 = LINE('',#4004,#4005); +#4004 = CARTESIAN_POINT('',(357.,650.,45.)); +#4005 = VECTOR('',#4006,1.); +#4006 = DIRECTION('',(0.,0.,1.)); +#4007 = ORIENTED_EDGE('',*,*,#4008,.T.); +#4008 = EDGE_CURVE('',#3999,#4009,#4011,.T.); +#4009 = VERTEX_POINT('',#4010); +#4010 = CARTESIAN_POINT('',(357.,682.,45.)); +#4011 = LINE('',#4012,#4013); +#4012 = CARTESIAN_POINT('',(357.,650.,45.)); +#4013 = VECTOR('',#4014,1.); +#4014 = DIRECTION('',(-0.,1.,0.)); +#4015 = ORIENTED_EDGE('',*,*,#4016,.T.); +#4016 = EDGE_CURVE('',#4009,#4017,#4019,.T.); +#4017 = VERTEX_POINT('',#4018); +#4018 = CARTESIAN_POINT('',(357.,682.,2.255E+03)); +#4019 = LINE('',#4020,#4021); +#4020 = CARTESIAN_POINT('',(357.,682.,45.)); +#4021 = VECTOR('',#4022,1.); +#4022 = DIRECTION('',(0.,0.,1.)); +#4023 = ORIENTED_EDGE('',*,*,#4024,.F.); +#4024 = EDGE_CURVE('',#4001,#4017,#4025,.T.); +#4025 = LINE('',#4026,#4027); +#4026 = CARTESIAN_POINT('',(357.,650.,2.255E+03)); +#4027 = VECTOR('',#4028,1.); +#4028 = DIRECTION('',(-0.,1.,0.)); +#4029 = PLANE('',#4030); +#4030 = AXIS2_PLACEMENT_3D('',#4031,#4032,#4033); +#4031 = CARTESIAN_POINT('',(357.,650.,45.)); +#4032 = DIRECTION('',(1.,0.,-0.)); +#4033 = DIRECTION('',(0.,0.,1.)); +#4034 = ADVANCED_FACE('',(#4035),#4051,.F.); +#4035 = FACE_BOUND('',#4036,.F.); +#4036 = EDGE_LOOP('',(#4037,#4043,#4044,#4050)); +#4037 = ORIENTED_EDGE('',*,*,#4038,.F.); +#4038 = EDGE_CURVE('',#3959,#3999,#4039,.T.); +#4039 = LINE('',#4040,#4041); +#4040 = CARTESIAN_POINT('',(-357.,650.,45.)); +#4041 = VECTOR('',#4042,1.); +#4042 = DIRECTION('',(1.,0.,-0.)); +#4043 = ORIENTED_EDGE('',*,*,#3958,.T.); +#4044 = ORIENTED_EDGE('',*,*,#4045,.T.); +#4045 = EDGE_CURVE('',#3961,#4001,#4046,.T.); +#4046 = LINE('',#4047,#4048); +#4047 = CARTESIAN_POINT('',(-357.,650.,2.255E+03)); +#4048 = VECTOR('',#4049,1.); +#4049 = DIRECTION('',(1.,0.,-0.)); +#4050 = ORIENTED_EDGE('',*,*,#3998,.F.); +#4051 = PLANE('',#4052); +#4052 = AXIS2_PLACEMENT_3D('',#4053,#4054,#4055); +#4053 = CARTESIAN_POINT('',(-357.,650.,45.)); +#4054 = DIRECTION('',(-0.,1.,0.)); +#4055 = DIRECTION('',(0.,0.,1.)); +#4056 = ADVANCED_FACE('',(#4057),#4073,.T.); +#4057 = FACE_BOUND('',#4058,.T.); +#4058 = EDGE_LOOP('',(#4059,#4065,#4066,#4072)); +#4059 = ORIENTED_EDGE('',*,*,#4060,.F.); +#4060 = EDGE_CURVE('',#3969,#4009,#4061,.T.); +#4061 = LINE('',#4062,#4063); +#4062 = CARTESIAN_POINT('',(-357.,682.,45.)); +#4063 = VECTOR('',#4064,1.); +#4064 = DIRECTION('',(1.,0.,-0.)); +#4065 = ORIENTED_EDGE('',*,*,#3976,.T.); +#4066 = ORIENTED_EDGE('',*,*,#4067,.T.); +#4067 = EDGE_CURVE('',#3977,#4017,#4068,.T.); +#4068 = LINE('',#4069,#4070); +#4069 = CARTESIAN_POINT('',(-357.,682.,2.255E+03)); +#4070 = VECTOR('',#4071,1.); +#4071 = DIRECTION('',(1.,0.,-0.)); +#4072 = ORIENTED_EDGE('',*,*,#4016,.F.); +#4073 = PLANE('',#4074); +#4074 = AXIS2_PLACEMENT_3D('',#4075,#4076,#4077); +#4075 = CARTESIAN_POINT('',(-357.,682.,45.)); +#4076 = DIRECTION('',(-0.,1.,0.)); +#4077 = DIRECTION('',(0.,0.,1.)); +#4078 = ADVANCED_FACE('',(#4079),#4085,.F.); +#4079 = FACE_BOUND('',#4080,.F.); +#4080 = EDGE_LOOP('',(#4081,#4082,#4083,#4084)); +#4081 = ORIENTED_EDGE('',*,*,#3968,.F.); +#4082 = ORIENTED_EDGE('',*,*,#4038,.T.); +#4083 = ORIENTED_EDGE('',*,*,#4008,.T.); +#4084 = ORIENTED_EDGE('',*,*,#4060,.F.); +#4085 = PLANE('',#4086); +#4086 = AXIS2_PLACEMENT_3D('',#4087,#4088,#4089); +#4087 = CARTESIAN_POINT('',(-357.,650.,45.)); +#4088 = DIRECTION('',(0.,0.,1.)); +#4089 = DIRECTION('',(1.,0.,-0.)); +#4090 = ADVANCED_FACE('',(#4091),#4097,.T.); +#4091 = FACE_BOUND('',#4092,.T.); +#4092 = EDGE_LOOP('',(#4093,#4094,#4095,#4096)); +#4093 = ORIENTED_EDGE('',*,*,#3984,.F.); +#4094 = ORIENTED_EDGE('',*,*,#4045,.T.); +#4095 = ORIENTED_EDGE('',*,*,#4024,.T.); +#4096 = ORIENTED_EDGE('',*,*,#4067,.F.); +#4097 = PLANE('',#4098); +#4098 = AXIS2_PLACEMENT_3D('',#4099,#4100,#4101); +#4099 = CARTESIAN_POINT('',(-357.,650.,2.255E+03)); +#4100 = DIRECTION('',(0.,0.,1.)); +#4101 = DIRECTION('',(1.,0.,-0.)); +#4102 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4106)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#4103,#4104,#4105)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#4103 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#4104 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#4105 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#4106 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4103, + 'distance_accuracy_value','confusion accuracy'); +#4107 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4108,#4110); +#4108 = ( REPRESENTATION_RELATIONSHIP('','',#3951,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4109) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#4109 = ITEM_DEFINED_TRANSFORMATION('','',#11,#27); +#4110 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #4111); +#4111 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('4','NAU03_Rear_Panel','',#5, + #3946,$); +#4112 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3948)); +#4113 = SHAPE_DEFINITION_REPRESENTATION(#4114,#4120); +#4114 = PRODUCT_DEFINITION_SHAPE('','',#4115); +#4115 = PRODUCT_DEFINITION('design','',#4116,#4119); +#4116 = PRODUCT_DEFINITION_FORMATION('','',#4117); +#4117 = PRODUCT('NAU03_Front_Left_Door','NAU03_Front_Left_Door','',( + #4118)); +#4118 = PRODUCT_CONTEXT('',#2,'mechanical'); +#4119 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#4120 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4121),#4691); +#4121 = MANIFOLD_SOLID_BREP('',#4122); +#4122 = CLOSED_SHELL('',(#4123,#4163,#4262,#4286,#4344,#4361,#4373,#4404 + ,#4435,#4466,#4490,#4514,#4531,#4543,#4560,#4577,#4608,#4632,#4656, + #4673)); +#4123 = ADVANCED_FACE('',(#4124),#4158,.F.); +#4124 = FACE_BOUND('',#4125,.F.); +#4125 = EDGE_LOOP('',(#4126,#4136,#4144,#4152)); +#4126 = ORIENTED_EDGE('',*,*,#4127,.F.); +#4127 = EDGE_CURVE('',#4128,#4130,#4132,.T.); +#4128 = VERTEX_POINT('',#4129); +#4129 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4130 = VERTEX_POINT('',#4131); +#4131 = CARTESIAN_POINT('',(-363.,-686.,2.25E+03)); +#4132 = LINE('',#4133,#4134); +#4133 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4134 = VECTOR('',#4135,1.); +#4135 = DIRECTION('',(0.,0.,1.)); +#4136 = ORIENTED_EDGE('',*,*,#4137,.T.); +#4137 = EDGE_CURVE('',#4128,#4138,#4140,.T.); +#4138 = VERTEX_POINT('',#4139); +#4139 = CARTESIAN_POINT('',(-363.,-650.,55.)); +#4140 = LINE('',#4141,#4142); +#4141 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4142 = VECTOR('',#4143,1.); +#4143 = DIRECTION('',(-0.,1.,0.)); +#4144 = ORIENTED_EDGE('',*,*,#4145,.T.); +#4145 = EDGE_CURVE('',#4138,#4146,#4148,.T.); +#4146 = VERTEX_POINT('',#4147); +#4147 = CARTESIAN_POINT('',(-363.,-650.,2.25E+03)); +#4148 = LINE('',#4149,#4150); +#4149 = CARTESIAN_POINT('',(-363.,-650.,55.)); +#4150 = VECTOR('',#4151,1.); +#4151 = DIRECTION('',(0.,0.,1.)); +#4152 = ORIENTED_EDGE('',*,*,#4153,.F.); +#4153 = EDGE_CURVE('',#4130,#4146,#4154,.T.); +#4154 = LINE('',#4155,#4156); +#4155 = CARTESIAN_POINT('',(-363.,-686.,2.25E+03)); +#4156 = VECTOR('',#4157,1.); +#4157 = DIRECTION('',(-0.,1.,0.)); +#4158 = PLANE('',#4159); +#4159 = AXIS2_PLACEMENT_3D('',#4160,#4161,#4162); +#4160 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4161 = DIRECTION('',(1.,0.,-0.)); +#4162 = DIRECTION('',(0.,0.,1.)); +#4163 = ADVANCED_FACE('',(#4164,#4189,#4223),#4257,.F.); +#4164 = FACE_BOUND('',#4165,.F.); +#4165 = EDGE_LOOP('',(#4166,#4174,#4175,#4183)); +#4166 = ORIENTED_EDGE('',*,*,#4167,.F.); +#4167 = EDGE_CURVE('',#4128,#4168,#4170,.T.); +#4168 = VERTEX_POINT('',#4169); +#4169 = CARTESIAN_POINT('',(-7.,-686.,55.)); +#4170 = LINE('',#4171,#4172); +#4171 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4172 = VECTOR('',#4173,1.); +#4173 = DIRECTION('',(1.,0.,-0.)); +#4174 = ORIENTED_EDGE('',*,*,#4127,.T.); +#4175 = ORIENTED_EDGE('',*,*,#4176,.T.); +#4176 = EDGE_CURVE('',#4130,#4177,#4179,.T.); +#4177 = VERTEX_POINT('',#4178); +#4178 = CARTESIAN_POINT('',(-7.,-686.,2.25E+03)); +#4179 = LINE('',#4180,#4181); +#4180 = CARTESIAN_POINT('',(-363.,-686.,2.25E+03)); +#4181 = VECTOR('',#4182,1.); +#4182 = DIRECTION('',(1.,0.,-0.)); +#4183 = ORIENTED_EDGE('',*,*,#4184,.F.); +#4184 = EDGE_CURVE('',#4168,#4177,#4185,.T.); +#4185 = LINE('',#4186,#4187); +#4186 = CARTESIAN_POINT('',(-7.,-686.,55.)); +#4187 = VECTOR('',#4188,1.); +#4188 = DIRECTION('',(0.,0.,1.)); +#4189 = FACE_BOUND('',#4190,.F.); +#4190 = EDGE_LOOP('',(#4191,#4201,#4209,#4217)); +#4191 = ORIENTED_EDGE('',*,*,#4192,.F.); +#4192 = EDGE_CURVE('',#4193,#4195,#4197,.T.); +#4193 = VERTEX_POINT('',#4194); +#4194 = CARTESIAN_POINT('',(-280.,-686.,1.797E+03)); +#4195 = VERTEX_POINT('',#4196); +#4196 = CARTESIAN_POINT('',(-71.,-686.,1.797E+03)); +#4197 = LINE('',#4198,#4199); +#4198 = CARTESIAN_POINT('',(-321.5,-686.,1.797E+03)); +#4199 = VECTOR('',#4200,1.); +#4200 = DIRECTION('',(1.,0.,-0.)); +#4201 = ORIENTED_EDGE('',*,*,#4202,.T.); +#4202 = EDGE_CURVE('',#4193,#4203,#4205,.T.); +#4203 = VERTEX_POINT('',#4204); +#4204 = CARTESIAN_POINT('',(-280.,-686.,1.303E+03)); +#4205 = LINE('',#4206,#4207); +#4206 = CARTESIAN_POINT('',(-280.,-686.,679.)); +#4207 = VECTOR('',#4208,1.); +#4208 = DIRECTION('',(-0.,0.,-1.)); +#4209 = ORIENTED_EDGE('',*,*,#4210,.T.); +#4210 = EDGE_CURVE('',#4203,#4211,#4213,.T.); +#4211 = VERTEX_POINT('',#4212); +#4212 = CARTESIAN_POINT('',(-71.,-686.,1.303E+03)); +#4213 = LINE('',#4214,#4215); +#4214 = CARTESIAN_POINT('',(-321.5,-686.,1.303E+03)); +#4215 = VECTOR('',#4216,1.); +#4216 = DIRECTION('',(1.,0.,-0.)); +#4217 = ORIENTED_EDGE('',*,*,#4218,.F.); +#4218 = EDGE_CURVE('',#4195,#4211,#4219,.T.); +#4219 = LINE('',#4220,#4221); +#4220 = CARTESIAN_POINT('',(-71.,-686.,920.)); +#4221 = VECTOR('',#4222,1.); +#4222 = DIRECTION('',(-0.,0.,-1.)); +#4223 = FACE_BOUND('',#4224,.F.); +#4224 = EDGE_LOOP('',(#4225,#4235,#4243,#4251)); +#4225 = ORIENTED_EDGE('',*,*,#4226,.F.); +#4226 = EDGE_CURVE('',#4227,#4229,#4231,.T.); +#4227 = VERTEX_POINT('',#4228); +#4228 = CARTESIAN_POINT('',(-49.,-686.,1.2286E+03)); +#4229 = VERTEX_POINT('',#4230); +#4230 = CARTESIAN_POINT('',(-33.,-686.,1.2286E+03)); +#4231 = LINE('',#4232,#4233); +#4232 = CARTESIAN_POINT('',(-206.,-686.,1.2286E+03)); +#4233 = VECTOR('',#4234,1.); +#4234 = DIRECTION('',(1.,0.,-0.)); +#4235 = ORIENTED_EDGE('',*,*,#4236,.T.); +#4236 = EDGE_CURVE('',#4227,#4237,#4239,.T.); +#4237 = VERTEX_POINT('',#4238); +#4238 = CARTESIAN_POINT('',(-49.,-686.,1.1086E+03)); +#4239 = LINE('',#4240,#4241); +#4240 = CARTESIAN_POINT('',(-49.,-686.,581.8)); +#4241 = VECTOR('',#4242,1.); +#4242 = DIRECTION('',(-0.,0.,-1.)); +#4243 = ORIENTED_EDGE('',*,*,#4244,.T.); +#4244 = EDGE_CURVE('',#4237,#4245,#4247,.T.); +#4245 = VERTEX_POINT('',#4246); +#4246 = CARTESIAN_POINT('',(-33.,-686.,1.1086E+03)); +#4247 = LINE('',#4248,#4249); +#4248 = CARTESIAN_POINT('',(-206.,-686.,1.1086E+03)); +#4249 = VECTOR('',#4250,1.); +#4250 = DIRECTION('',(1.,0.,-0.)); +#4251 = ORIENTED_EDGE('',*,*,#4252,.F.); +#4252 = EDGE_CURVE('',#4229,#4245,#4253,.T.); +#4253 = LINE('',#4254,#4255); +#4254 = CARTESIAN_POINT('',(-33.,-686.,581.8)); +#4255 = VECTOR('',#4256,1.); +#4256 = DIRECTION('',(-0.,0.,-1.)); +#4257 = PLANE('',#4258); +#4258 = AXIS2_PLACEMENT_3D('',#4259,#4260,#4261); +#4259 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4260 = DIRECTION('',(-0.,1.,0.)); +#4261 = DIRECTION('',(0.,0.,1.)); +#4262 = ADVANCED_FACE('',(#4263),#4281,.T.); +#4263 = FACE_BOUND('',#4264,.T.); +#4264 = EDGE_LOOP('',(#4265,#4266,#4267,#4275)); +#4265 = ORIENTED_EDGE('',*,*,#4153,.F.); +#4266 = ORIENTED_EDGE('',*,*,#4176,.T.); +#4267 = ORIENTED_EDGE('',*,*,#4268,.T.); +#4268 = EDGE_CURVE('',#4177,#4269,#4271,.T.); +#4269 = VERTEX_POINT('',#4270); +#4270 = CARTESIAN_POINT('',(-7.,-650.,2.25E+03)); +#4271 = LINE('',#4272,#4273); +#4272 = CARTESIAN_POINT('',(-7.,-686.,2.25E+03)); +#4273 = VECTOR('',#4274,1.); +#4274 = DIRECTION('',(-0.,1.,0.)); +#4275 = ORIENTED_EDGE('',*,*,#4276,.F.); +#4276 = EDGE_CURVE('',#4146,#4269,#4277,.T.); +#4277 = LINE('',#4278,#4279); +#4278 = CARTESIAN_POINT('',(-363.,-650.,2.25E+03)); +#4279 = VECTOR('',#4280,1.); +#4280 = DIRECTION('',(1.,0.,-0.)); +#4281 = PLANE('',#4282); +#4282 = AXIS2_PLACEMENT_3D('',#4283,#4284,#4285); +#4283 = CARTESIAN_POINT('',(-363.,-686.,2.25E+03)); +#4284 = DIRECTION('',(0.,0.,1.)); +#4285 = DIRECTION('',(1.,0.,-0.)); +#4286 = ADVANCED_FACE('',(#4287,#4305),#4339,.T.); +#4287 = FACE_BOUND('',#4288,.T.); +#4288 = EDGE_LOOP('',(#4289,#4297,#4298,#4299)); +#4289 = ORIENTED_EDGE('',*,*,#4290,.F.); +#4290 = EDGE_CURVE('',#4138,#4291,#4293,.T.); +#4291 = VERTEX_POINT('',#4292); +#4292 = CARTESIAN_POINT('',(-7.,-650.,55.)); +#4293 = LINE('',#4294,#4295); +#4294 = CARTESIAN_POINT('',(-363.,-650.,55.)); +#4295 = VECTOR('',#4296,1.); +#4296 = DIRECTION('',(1.,0.,-0.)); +#4297 = ORIENTED_EDGE('',*,*,#4145,.T.); +#4298 = ORIENTED_EDGE('',*,*,#4276,.T.); +#4299 = ORIENTED_EDGE('',*,*,#4300,.F.); +#4300 = EDGE_CURVE('',#4291,#4269,#4301,.T.); +#4301 = LINE('',#4302,#4303); +#4302 = CARTESIAN_POINT('',(-7.,-650.,55.)); +#4303 = VECTOR('',#4304,1.); +#4304 = DIRECTION('',(0.,0.,1.)); +#4305 = FACE_BOUND('',#4306,.T.); +#4306 = EDGE_LOOP('',(#4307,#4317,#4325,#4333)); +#4307 = ORIENTED_EDGE('',*,*,#4308,.F.); +#4308 = EDGE_CURVE('',#4309,#4311,#4313,.T.); +#4309 = VERTEX_POINT('',#4310); +#4310 = CARTESIAN_POINT('',(-268.,-650.,1.785E+03)); +#4311 = VERTEX_POINT('',#4312); +#4312 = CARTESIAN_POINT('',(-83.,-650.,1.785E+03)); +#4313 = LINE('',#4314,#4315); +#4314 = CARTESIAN_POINT('',(-315.5,-650.,1.785E+03)); +#4315 = VECTOR('',#4316,1.); +#4316 = DIRECTION('',(1.,0.,-0.)); +#4317 = ORIENTED_EDGE('',*,*,#4318,.T.); +#4318 = EDGE_CURVE('',#4309,#4319,#4321,.T.); +#4319 = VERTEX_POINT('',#4320); +#4320 = CARTESIAN_POINT('',(-268.,-650.,1.315E+03)); +#4321 = LINE('',#4322,#4323); +#4322 = CARTESIAN_POINT('',(-268.,-650.,685.)); +#4323 = VECTOR('',#4324,1.); +#4324 = DIRECTION('',(-0.,0.,-1.)); +#4325 = ORIENTED_EDGE('',*,*,#4326,.T.); +#4326 = EDGE_CURVE('',#4319,#4327,#4329,.T.); +#4327 = VERTEX_POINT('',#4328); +#4328 = CARTESIAN_POINT('',(-83.,-650.,1.315E+03)); +#4329 = LINE('',#4330,#4331); +#4330 = CARTESIAN_POINT('',(-315.5,-650.,1.315E+03)); +#4331 = VECTOR('',#4332,1.); +#4332 = DIRECTION('',(1.,0.,-0.)); +#4333 = ORIENTED_EDGE('',*,*,#4334,.F.); +#4334 = EDGE_CURVE('',#4311,#4327,#4335,.T.); +#4335 = LINE('',#4336,#4337); +#4336 = CARTESIAN_POINT('',(-83.,-650.,685.)); +#4337 = VECTOR('',#4338,1.); +#4338 = DIRECTION('',(-0.,0.,-1.)); +#4339 = PLANE('',#4340); +#4340 = AXIS2_PLACEMENT_3D('',#4341,#4342,#4343); +#4341 = CARTESIAN_POINT('',(-363.,-650.,55.)); +#4342 = DIRECTION('',(-0.,1.,0.)); +#4343 = DIRECTION('',(0.,0.,1.)); +#4344 = ADVANCED_FACE('',(#4345),#4356,.F.); +#4345 = FACE_BOUND('',#4346,.F.); +#4346 = EDGE_LOOP('',(#4347,#4348,#4349,#4355)); +#4347 = ORIENTED_EDGE('',*,*,#4137,.F.); +#4348 = ORIENTED_EDGE('',*,*,#4167,.T.); +#4349 = ORIENTED_EDGE('',*,*,#4350,.T.); +#4350 = EDGE_CURVE('',#4168,#4291,#4351,.T.); +#4351 = LINE('',#4352,#4353); +#4352 = CARTESIAN_POINT('',(-7.,-686.,55.)); +#4353 = VECTOR('',#4354,1.); +#4354 = DIRECTION('',(-0.,1.,0.)); +#4355 = ORIENTED_EDGE('',*,*,#4290,.F.); +#4356 = PLANE('',#4357); +#4357 = AXIS2_PLACEMENT_3D('',#4358,#4359,#4360); +#4358 = CARTESIAN_POINT('',(-363.,-686.,55.)); +#4359 = DIRECTION('',(0.,0.,1.)); +#4360 = DIRECTION('',(1.,0.,-0.)); +#4361 = ADVANCED_FACE('',(#4362),#4368,.T.); +#4362 = FACE_BOUND('',#4363,.T.); +#4363 = EDGE_LOOP('',(#4364,#4365,#4366,#4367)); +#4364 = ORIENTED_EDGE('',*,*,#4184,.F.); +#4365 = ORIENTED_EDGE('',*,*,#4350,.T.); +#4366 = ORIENTED_EDGE('',*,*,#4300,.T.); +#4367 = ORIENTED_EDGE('',*,*,#4268,.F.); +#4368 = PLANE('',#4369); +#4369 = AXIS2_PLACEMENT_3D('',#4370,#4371,#4372); +#4370 = CARTESIAN_POINT('',(-7.,-686.,55.)); +#4371 = DIRECTION('',(1.,0.,-0.)); +#4372 = DIRECTION('',(0.,0.,1.)); +#4373 = ADVANCED_FACE('',(#4374),#4399,.T.); +#4374 = FACE_BOUND('',#4375,.T.); +#4375 = EDGE_LOOP('',(#4376,#4384,#4392,#4398)); +#4376 = ORIENTED_EDGE('',*,*,#4377,.F.); +#4377 = EDGE_CURVE('',#4378,#4193,#4380,.T.); +#4378 = VERTEX_POINT('',#4379); +#4379 = CARTESIAN_POINT('',(-280.,-690.,1.797E+03)); +#4380 = LINE('',#4381,#4382); +#4381 = CARTESIAN_POINT('',(-280.,-690.,1.797E+03)); +#4382 = VECTOR('',#4383,1.); +#4383 = DIRECTION('',(-0.,1.,0.)); +#4384 = ORIENTED_EDGE('',*,*,#4385,.T.); +#4385 = EDGE_CURVE('',#4378,#4386,#4388,.T.); +#4386 = VERTEX_POINT('',#4387); +#4387 = CARTESIAN_POINT('',(-71.,-690.,1.797E+03)); +#4388 = LINE('',#4389,#4390); +#4389 = CARTESIAN_POINT('',(-280.,-690.,1.797E+03)); +#4390 = VECTOR('',#4391,1.); +#4391 = DIRECTION('',(1.,0.,-0.)); +#4392 = ORIENTED_EDGE('',*,*,#4393,.T.); +#4393 = EDGE_CURVE('',#4386,#4195,#4394,.T.); +#4394 = LINE('',#4395,#4396); +#4395 = CARTESIAN_POINT('',(-71.,-690.,1.797E+03)); +#4396 = VECTOR('',#4397,1.); +#4397 = DIRECTION('',(-0.,1.,0.)); +#4398 = ORIENTED_EDGE('',*,*,#4192,.F.); +#4399 = PLANE('',#4400); +#4400 = AXIS2_PLACEMENT_3D('',#4401,#4402,#4403); +#4401 = CARTESIAN_POINT('',(-280.,-690.,1.797E+03)); +#4402 = DIRECTION('',(0.,0.,1.)); +#4403 = DIRECTION('',(1.,0.,-0.)); +#4404 = ADVANCED_FACE('',(#4405),#4430,.F.); +#4405 = FACE_BOUND('',#4406,.F.); +#4406 = EDGE_LOOP('',(#4407,#4415,#4423,#4429)); +#4407 = ORIENTED_EDGE('',*,*,#4408,.F.); +#4408 = EDGE_CURVE('',#4409,#4203,#4411,.T.); +#4409 = VERTEX_POINT('',#4410); +#4410 = CARTESIAN_POINT('',(-280.,-690.,1.303E+03)); +#4411 = LINE('',#4412,#4413); +#4412 = CARTESIAN_POINT('',(-280.,-690.,1.303E+03)); +#4413 = VECTOR('',#4414,1.); +#4414 = DIRECTION('',(-0.,1.,0.)); +#4415 = ORIENTED_EDGE('',*,*,#4416,.T.); +#4416 = EDGE_CURVE('',#4409,#4417,#4419,.T.); +#4417 = VERTEX_POINT('',#4418); +#4418 = CARTESIAN_POINT('',(-71.,-690.,1.303E+03)); +#4419 = LINE('',#4420,#4421); +#4420 = CARTESIAN_POINT('',(-280.,-690.,1.303E+03)); +#4421 = VECTOR('',#4422,1.); +#4422 = DIRECTION('',(1.,0.,-0.)); +#4423 = ORIENTED_EDGE('',*,*,#4424,.T.); +#4424 = EDGE_CURVE('',#4417,#4211,#4425,.T.); +#4425 = LINE('',#4426,#4427); +#4426 = CARTESIAN_POINT('',(-71.,-690.,1.303E+03)); +#4427 = VECTOR('',#4428,1.); +#4428 = DIRECTION('',(-0.,1.,0.)); +#4429 = ORIENTED_EDGE('',*,*,#4210,.F.); +#4430 = PLANE('',#4431); +#4431 = AXIS2_PLACEMENT_3D('',#4432,#4433,#4434); +#4432 = CARTESIAN_POINT('',(-280.,-690.,1.303E+03)); +#4433 = DIRECTION('',(0.,0.,1.)); +#4434 = DIRECTION('',(1.,0.,-0.)); +#4435 = ADVANCED_FACE('',(#4436),#4461,.T.); +#4436 = FACE_BOUND('',#4437,.T.); +#4437 = EDGE_LOOP('',(#4438,#4446,#4454,#4460)); +#4438 = ORIENTED_EDGE('',*,*,#4439,.F.); +#4439 = EDGE_CURVE('',#4440,#4227,#4442,.T.); +#4440 = VERTEX_POINT('',#4441); +#4441 = CARTESIAN_POINT('',(-49.,-698.,1.2286E+03)); +#4442 = LINE('',#4443,#4444); +#4443 = CARTESIAN_POINT('',(-49.,-698.,1.2286E+03)); +#4444 = VECTOR('',#4445,1.); +#4445 = DIRECTION('',(-0.,1.,0.)); +#4446 = ORIENTED_EDGE('',*,*,#4447,.T.); +#4447 = EDGE_CURVE('',#4440,#4448,#4450,.T.); +#4448 = VERTEX_POINT('',#4449); +#4449 = CARTESIAN_POINT('',(-33.,-698.,1.2286E+03)); +#4450 = LINE('',#4451,#4452); +#4451 = CARTESIAN_POINT('',(-49.,-698.,1.2286E+03)); +#4452 = VECTOR('',#4453,1.); +#4453 = DIRECTION('',(1.,0.,-0.)); +#4454 = ORIENTED_EDGE('',*,*,#4455,.T.); +#4455 = EDGE_CURVE('',#4448,#4229,#4456,.T.); +#4456 = LINE('',#4457,#4458); +#4457 = CARTESIAN_POINT('',(-33.,-698.,1.2286E+03)); +#4458 = VECTOR('',#4459,1.); +#4459 = DIRECTION('',(-0.,1.,0.)); +#4460 = ORIENTED_EDGE('',*,*,#4226,.F.); +#4461 = PLANE('',#4462); +#4462 = AXIS2_PLACEMENT_3D('',#4463,#4464,#4465); +#4463 = CARTESIAN_POINT('',(-49.,-698.,1.2286E+03)); +#4464 = DIRECTION('',(0.,0.,1.)); +#4465 = DIRECTION('',(1.,0.,-0.)); +#4466 = ADVANCED_FACE('',(#4467),#4485,.F.); +#4467 = FACE_BOUND('',#4468,.F.); +#4468 = EDGE_LOOP('',(#4469,#4470,#4478,#4484)); +#4469 = ORIENTED_EDGE('',*,*,#4439,.F.); +#4470 = ORIENTED_EDGE('',*,*,#4471,.F.); +#4471 = EDGE_CURVE('',#4472,#4440,#4474,.T.); +#4472 = VERTEX_POINT('',#4473); +#4473 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4474 = LINE('',#4475,#4476); +#4475 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4476 = VECTOR('',#4477,1.); +#4477 = DIRECTION('',(0.,0.,1.)); +#4478 = ORIENTED_EDGE('',*,*,#4479,.T.); +#4479 = EDGE_CURVE('',#4472,#4237,#4480,.T.); +#4480 = LINE('',#4481,#4482); +#4481 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4482 = VECTOR('',#4483,1.); +#4483 = DIRECTION('',(-0.,1.,0.)); +#4484 = ORIENTED_EDGE('',*,*,#4236,.F.); +#4485 = PLANE('',#4486); +#4486 = AXIS2_PLACEMENT_3D('',#4487,#4488,#4489); +#4487 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4488 = DIRECTION('',(1.,0.,-0.)); +#4489 = DIRECTION('',(0.,0.,1.)); +#4490 = ADVANCED_FACE('',(#4491),#4509,.T.); +#4491 = FACE_BOUND('',#4492,.T.); +#4492 = EDGE_LOOP('',(#4493,#4494,#4502,#4508)); +#4493 = ORIENTED_EDGE('',*,*,#4455,.F.); +#4494 = ORIENTED_EDGE('',*,*,#4495,.F.); +#4495 = EDGE_CURVE('',#4496,#4448,#4498,.T.); +#4496 = VERTEX_POINT('',#4497); +#4497 = CARTESIAN_POINT('',(-33.,-698.,1.1086E+03)); +#4498 = LINE('',#4499,#4500); +#4499 = CARTESIAN_POINT('',(-33.,-698.,1.1086E+03)); +#4500 = VECTOR('',#4501,1.); +#4501 = DIRECTION('',(0.,0.,1.)); +#4502 = ORIENTED_EDGE('',*,*,#4503,.T.); +#4503 = EDGE_CURVE('',#4496,#4245,#4504,.T.); +#4504 = LINE('',#4505,#4506); +#4505 = CARTESIAN_POINT('',(-33.,-698.,1.1086E+03)); +#4506 = VECTOR('',#4507,1.); +#4507 = DIRECTION('',(-0.,1.,0.)); +#4508 = ORIENTED_EDGE('',*,*,#4252,.F.); +#4509 = PLANE('',#4510); +#4510 = AXIS2_PLACEMENT_3D('',#4511,#4512,#4513); +#4511 = CARTESIAN_POINT('',(-33.,-698.,1.1086E+03)); +#4512 = DIRECTION('',(1.,0.,-0.)); +#4513 = DIRECTION('',(0.,0.,1.)); +#4514 = ADVANCED_FACE('',(#4515),#4526,.F.); +#4515 = FACE_BOUND('',#4516,.F.); +#4516 = EDGE_LOOP('',(#4517,#4518,#4524,#4525)); +#4517 = ORIENTED_EDGE('',*,*,#4479,.F.); +#4518 = ORIENTED_EDGE('',*,*,#4519,.T.); +#4519 = EDGE_CURVE('',#4472,#4496,#4520,.T.); +#4520 = LINE('',#4521,#4522); +#4521 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4522 = VECTOR('',#4523,1.); +#4523 = DIRECTION('',(1.,0.,-0.)); +#4524 = ORIENTED_EDGE('',*,*,#4503,.T.); +#4525 = ORIENTED_EDGE('',*,*,#4244,.F.); +#4526 = PLANE('',#4527); +#4527 = AXIS2_PLACEMENT_3D('',#4528,#4529,#4530); +#4528 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4529 = DIRECTION('',(0.,0.,1.)); +#4530 = DIRECTION('',(1.,0.,-0.)); +#4531 = ADVANCED_FACE('',(#4532),#4538,.F.); +#4532 = FACE_BOUND('',#4533,.F.); +#4533 = EDGE_LOOP('',(#4534,#4535,#4536,#4537)); +#4534 = ORIENTED_EDGE('',*,*,#4519,.F.); +#4535 = ORIENTED_EDGE('',*,*,#4471,.T.); +#4536 = ORIENTED_EDGE('',*,*,#4447,.T.); +#4537 = ORIENTED_EDGE('',*,*,#4495,.F.); +#4538 = PLANE('',#4539); +#4539 = AXIS2_PLACEMENT_3D('',#4540,#4541,#4542); +#4540 = CARTESIAN_POINT('',(-49.,-698.,1.1086E+03)); +#4541 = DIRECTION('',(-0.,1.,0.)); +#4542 = DIRECTION('',(0.,0.,1.)); +#4543 = ADVANCED_FACE('',(#4544),#4555,.T.); +#4544 = FACE_BOUND('',#4545,.T.); +#4545 = EDGE_LOOP('',(#4546,#4547,#4553,#4554)); +#4546 = ORIENTED_EDGE('',*,*,#4393,.F.); +#4547 = ORIENTED_EDGE('',*,*,#4548,.F.); +#4548 = EDGE_CURVE('',#4417,#4386,#4549,.T.); +#4549 = LINE('',#4550,#4551); +#4550 = CARTESIAN_POINT('',(-71.,-690.,1.785E+03)); +#4551 = VECTOR('',#4552,1.); +#4552 = DIRECTION('',(0.,0.,1.)); +#4553 = ORIENTED_EDGE('',*,*,#4424,.T.); +#4554 = ORIENTED_EDGE('',*,*,#4218,.F.); +#4555 = PLANE('',#4556); +#4556 = AXIS2_PLACEMENT_3D('',#4557,#4558,#4559); +#4557 = CARTESIAN_POINT('',(-71.,-688.,1.55E+03)); +#4558 = DIRECTION('',(1.,0.,0.)); +#4559 = DIRECTION('',(-0.,0.,1.)); +#4560 = ADVANCED_FACE('',(#4561),#4572,.T.); +#4561 = FACE_BOUND('',#4562,.T.); +#4562 = EDGE_LOOP('',(#4563,#4564,#4565,#4571)); +#4563 = ORIENTED_EDGE('',*,*,#4202,.T.); +#4564 = ORIENTED_EDGE('',*,*,#4408,.F.); +#4565 = ORIENTED_EDGE('',*,*,#4566,.T.); +#4566 = EDGE_CURVE('',#4409,#4378,#4567,.T.); +#4567 = LINE('',#4568,#4569); +#4568 = CARTESIAN_POINT('',(-280.,-690.,1.303E+03)); +#4569 = VECTOR('',#4570,1.); +#4570 = DIRECTION('',(0.,0.,1.)); +#4571 = ORIENTED_EDGE('',*,*,#4377,.T.); +#4572 = PLANE('',#4573); +#4573 = AXIS2_PLACEMENT_3D('',#4574,#4575,#4576); +#4574 = CARTESIAN_POINT('',(-280.,-688.,1.55E+03)); +#4575 = DIRECTION('',(-1.,-0.,-0.)); +#4576 = DIRECTION('',(0.,0.,-1.)); +#4577 = ADVANCED_FACE('',(#4578),#4603,.T.); +#4578 = FACE_BOUND('',#4579,.T.); +#4579 = EDGE_LOOP('',(#4580,#4581,#4589,#4597)); +#4580 = ORIENTED_EDGE('',*,*,#4308,.T.); +#4581 = ORIENTED_EDGE('',*,*,#4582,.F.); +#4582 = EDGE_CURVE('',#4583,#4311,#4585,.T.); +#4583 = VERTEX_POINT('',#4584); +#4584 = CARTESIAN_POINT('',(-83.,-690.,1.785E+03)); +#4585 = LINE('',#4586,#4587); +#4586 = CARTESIAN_POINT('',(-83.,-689.,1.785E+03)); +#4587 = VECTOR('',#4588,1.); +#4588 = DIRECTION('',(-0.,1.,0.)); +#4589 = ORIENTED_EDGE('',*,*,#4590,.F.); +#4590 = EDGE_CURVE('',#4591,#4583,#4593,.T.); +#4591 = VERTEX_POINT('',#4592); +#4592 = CARTESIAN_POINT('',(-268.,-690.,1.785E+03)); +#4593 = LINE('',#4594,#4595); +#4594 = CARTESIAN_POINT('',(-280.,-690.,1.785E+03)); +#4595 = VECTOR('',#4596,1.); +#4596 = DIRECTION('',(1.,0.,-0.)); +#4597 = ORIENTED_EDGE('',*,*,#4598,.T.); +#4598 = EDGE_CURVE('',#4591,#4309,#4599,.T.); +#4599 = LINE('',#4600,#4601); +#4600 = CARTESIAN_POINT('',(-268.,-690.,1.785E+03)); +#4601 = VECTOR('',#4602,1.); +#4602 = DIRECTION('',(-0.,1.,0.)); +#4603 = PLANE('',#4604); +#4604 = AXIS2_PLACEMENT_3D('',#4605,#4606,#4607); +#4605 = CARTESIAN_POINT('',(-175.5,-670.,1.785E+03)); +#4606 = DIRECTION('',(-0.,-0.,-1.)); +#4607 = DIRECTION('',(-1.,0.,0.)); +#4608 = ADVANCED_FACE('',(#4609),#4627,.T.); +#4609 = FACE_BOUND('',#4610,.T.); +#4610 = EDGE_LOOP('',(#4611,#4612,#4613,#4621)); +#4611 = ORIENTED_EDGE('',*,*,#4582,.T.); +#4612 = ORIENTED_EDGE('',*,*,#4334,.T.); +#4613 = ORIENTED_EDGE('',*,*,#4614,.F.); +#4614 = EDGE_CURVE('',#4615,#4327,#4617,.T.); +#4615 = VERTEX_POINT('',#4616); +#4616 = CARTESIAN_POINT('',(-83.,-690.,1.315E+03)); +#4617 = LINE('',#4618,#4619); +#4618 = CARTESIAN_POINT('',(-83.,-689.,1.315E+03)); +#4619 = VECTOR('',#4620,1.); +#4620 = DIRECTION('',(-0.,1.,0.)); +#4621 = ORIENTED_EDGE('',*,*,#4622,.T.); +#4622 = EDGE_CURVE('',#4615,#4583,#4623,.T.); +#4623 = LINE('',#4624,#4625); +#4624 = CARTESIAN_POINT('',(-83.,-690.,1.315E+03)); +#4625 = VECTOR('',#4626,1.); +#4626 = DIRECTION('',(0.,0.,1.)); +#4627 = PLANE('',#4628); +#4628 = AXIS2_PLACEMENT_3D('',#4629,#4630,#4631); +#4629 = CARTESIAN_POINT('',(-83.,-670.,1.55E+03)); +#4630 = DIRECTION('',(-1.,-0.,-0.)); +#4631 = DIRECTION('',(0.,0.,-1.)); +#4632 = ADVANCED_FACE('',(#4633),#4651,.T.); +#4633 = FACE_BOUND('',#4634,.T.); +#4634 = EDGE_LOOP('',(#4635,#4643,#4649,#4650)); +#4635 = ORIENTED_EDGE('',*,*,#4636,.F.); +#4636 = EDGE_CURVE('',#4637,#4319,#4639,.T.); +#4637 = VERTEX_POINT('',#4638); +#4638 = CARTESIAN_POINT('',(-268.,-690.,1.315E+03)); +#4639 = LINE('',#4640,#4641); +#4640 = CARTESIAN_POINT('',(-268.,-689.,1.315E+03)); +#4641 = VECTOR('',#4642,1.); +#4642 = DIRECTION('',(-0.,1.,0.)); +#4643 = ORIENTED_EDGE('',*,*,#4644,.T.); +#4644 = EDGE_CURVE('',#4637,#4615,#4645,.T.); +#4645 = LINE('',#4646,#4647); +#4646 = CARTESIAN_POINT('',(-280.,-690.,1.315E+03)); +#4647 = VECTOR('',#4648,1.); +#4648 = DIRECTION('',(1.,0.,-0.)); +#4649 = ORIENTED_EDGE('',*,*,#4614,.T.); +#4650 = ORIENTED_EDGE('',*,*,#4326,.F.); +#4651 = PLANE('',#4652); +#4652 = AXIS2_PLACEMENT_3D('',#4653,#4654,#4655); +#4653 = CARTESIAN_POINT('',(-175.5,-670.,1.315E+03)); +#4654 = DIRECTION('',(0.,0.,1.)); +#4655 = DIRECTION('',(1.,0.,-0.)); +#4656 = ADVANCED_FACE('',(#4657),#4668,.T.); +#4657 = FACE_BOUND('',#4658,.T.); +#4658 = EDGE_LOOP('',(#4659,#4660,#4666,#4667)); +#4659 = ORIENTED_EDGE('',*,*,#4598,.F.); +#4660 = ORIENTED_EDGE('',*,*,#4661,.F.); +#4661 = EDGE_CURVE('',#4637,#4591,#4662,.T.); +#4662 = LINE('',#4663,#4664); +#4663 = CARTESIAN_POINT('',(-268.,-690.,1.315E+03)); +#4664 = VECTOR('',#4665,1.); +#4665 = DIRECTION('',(0.,0.,1.)); +#4666 = ORIENTED_EDGE('',*,*,#4636,.T.); +#4667 = ORIENTED_EDGE('',*,*,#4318,.F.); +#4668 = PLANE('',#4669); +#4669 = AXIS2_PLACEMENT_3D('',#4670,#4671,#4672); +#4670 = CARTESIAN_POINT('',(-268.,-670.,1.55E+03)); +#4671 = DIRECTION('',(1.,0.,0.)); +#4672 = DIRECTION('',(-0.,0.,1.)); +#4673 = ADVANCED_FACE('',(#4674,#4680),#4686,.T.); +#4674 = FACE_BOUND('',#4675,.T.); +#4675 = EDGE_LOOP('',(#4676,#4677,#4678,#4679)); +#4676 = ORIENTED_EDGE('',*,*,#4548,.T.); +#4677 = ORIENTED_EDGE('',*,*,#4385,.F.); +#4678 = ORIENTED_EDGE('',*,*,#4566,.F.); +#4679 = ORIENTED_EDGE('',*,*,#4416,.T.); +#4680 = FACE_BOUND('',#4681,.T.); +#4681 = EDGE_LOOP('',(#4682,#4683,#4684,#4685)); +#4682 = ORIENTED_EDGE('',*,*,#4590,.T.); +#4683 = ORIENTED_EDGE('',*,*,#4622,.F.); +#4684 = ORIENTED_EDGE('',*,*,#4644,.F.); +#4685 = ORIENTED_EDGE('',*,*,#4661,.T.); +#4686 = PLANE('',#4687); +#4687 = AXIS2_PLACEMENT_3D('',#4688,#4689,#4690); +#4688 = CARTESIAN_POINT('',(-175.5,-690.,1.55E+03)); +#4689 = DIRECTION('',(-0.,-1.,-0.)); +#4690 = DIRECTION('',(0.,0.,-1.)); +#4691 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4695)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#4692,#4693,#4694)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#4692 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#4693 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#4694 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#4695 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4692, + 'distance_accuracy_value','confusion accuracy'); +#4696 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4697,#4699); +#4697 = ( REPRESENTATION_RELATIONSHIP('','',#4120,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4698) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#4698 = ITEM_DEFINED_TRANSFORMATION('','',#11,#31); +#4699 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #4700); +#4700 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('5','NAU03_Front_Left_Door','',#5 + ,#4115,$); +#4701 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4117)); +#4702 = SHAPE_DEFINITION_REPRESENTATION(#4703,#4709); +#4703 = PRODUCT_DEFINITION_SHAPE('','',#4704); +#4704 = PRODUCT_DEFINITION('design','',#4705,#4708); +#4705 = PRODUCT_DEFINITION_FORMATION('','',#4706); +#4706 = PRODUCT('NAU03_Front_Right_Door','NAU03_Front_Right_Door','',( + #4707)); +#4707 = PRODUCT_CONTEXT('',#2,'mechanical'); +#4708 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#4709 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4710),#5280); +#4710 = MANIFOLD_SOLID_BREP('',#4711); +#4711 = CLOSED_SHELL('',(#4712,#4752,#4851,#4875,#4933,#4950,#4962,#4993 + ,#5024,#5055,#5079,#5103,#5120,#5132,#5149,#5166,#5197,#5221,#5245, + #5262)); +#4712 = ADVANCED_FACE('',(#4713),#4747,.F.); +#4713 = FACE_BOUND('',#4714,.F.); +#4714 = EDGE_LOOP('',(#4715,#4725,#4733,#4741)); +#4715 = ORIENTED_EDGE('',*,*,#4716,.F.); +#4716 = EDGE_CURVE('',#4717,#4719,#4721,.T.); +#4717 = VERTEX_POINT('',#4718); +#4718 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4719 = VERTEX_POINT('',#4720); +#4720 = CARTESIAN_POINT('',(1.,-686.,2.25E+03)); +#4721 = LINE('',#4722,#4723); +#4722 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4723 = VECTOR('',#4724,1.); +#4724 = DIRECTION('',(0.,0.,1.)); +#4725 = ORIENTED_EDGE('',*,*,#4726,.T.); +#4726 = EDGE_CURVE('',#4717,#4727,#4729,.T.); +#4727 = VERTEX_POINT('',#4728); +#4728 = CARTESIAN_POINT('',(1.,-650.,55.)); +#4729 = LINE('',#4730,#4731); +#4730 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4731 = VECTOR('',#4732,1.); +#4732 = DIRECTION('',(-0.,1.,0.)); +#4733 = ORIENTED_EDGE('',*,*,#4734,.T.); +#4734 = EDGE_CURVE('',#4727,#4735,#4737,.T.); +#4735 = VERTEX_POINT('',#4736); +#4736 = CARTESIAN_POINT('',(1.,-650.,2.25E+03)); +#4737 = LINE('',#4738,#4739); +#4738 = CARTESIAN_POINT('',(1.,-650.,55.)); +#4739 = VECTOR('',#4740,1.); +#4740 = DIRECTION('',(0.,0.,1.)); +#4741 = ORIENTED_EDGE('',*,*,#4742,.F.); +#4742 = EDGE_CURVE('',#4719,#4735,#4743,.T.); +#4743 = LINE('',#4744,#4745); +#4744 = CARTESIAN_POINT('',(1.,-686.,2.25E+03)); +#4745 = VECTOR('',#4746,1.); +#4746 = DIRECTION('',(-0.,1.,0.)); +#4747 = PLANE('',#4748); +#4748 = AXIS2_PLACEMENT_3D('',#4749,#4750,#4751); +#4749 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4750 = DIRECTION('',(1.,0.,-0.)); +#4751 = DIRECTION('',(0.,0.,1.)); +#4752 = ADVANCED_FACE('',(#4753,#4778,#4812),#4846,.F.); +#4753 = FACE_BOUND('',#4754,.F.); +#4754 = EDGE_LOOP('',(#4755,#4763,#4764,#4772)); +#4755 = ORIENTED_EDGE('',*,*,#4756,.F.); +#4756 = EDGE_CURVE('',#4717,#4757,#4759,.T.); +#4757 = VERTEX_POINT('',#4758); +#4758 = CARTESIAN_POINT('',(357.,-686.,55.)); +#4759 = LINE('',#4760,#4761); +#4760 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4761 = VECTOR('',#4762,1.); +#4762 = DIRECTION('',(1.,0.,-0.)); +#4763 = ORIENTED_EDGE('',*,*,#4716,.T.); +#4764 = ORIENTED_EDGE('',*,*,#4765,.T.); +#4765 = EDGE_CURVE('',#4719,#4766,#4768,.T.); +#4766 = VERTEX_POINT('',#4767); +#4767 = CARTESIAN_POINT('',(357.,-686.,2.25E+03)); +#4768 = LINE('',#4769,#4770); +#4769 = CARTESIAN_POINT('',(1.,-686.,2.25E+03)); +#4770 = VECTOR('',#4771,1.); +#4771 = DIRECTION('',(1.,0.,-0.)); +#4772 = ORIENTED_EDGE('',*,*,#4773,.F.); +#4773 = EDGE_CURVE('',#4757,#4766,#4774,.T.); +#4774 = LINE('',#4775,#4776); +#4775 = CARTESIAN_POINT('',(357.,-686.,55.)); +#4776 = VECTOR('',#4777,1.); +#4777 = DIRECTION('',(0.,0.,1.)); +#4778 = FACE_BOUND('',#4779,.F.); +#4779 = EDGE_LOOP('',(#4780,#4790,#4798,#4806)); +#4780 = ORIENTED_EDGE('',*,*,#4781,.F.); +#4781 = EDGE_CURVE('',#4782,#4784,#4786,.T.); +#4782 = VERTEX_POINT('',#4783); +#4783 = CARTESIAN_POINT('',(59.,-686.,1.797E+03)); +#4784 = VERTEX_POINT('',#4785); +#4785 = CARTESIAN_POINT('',(268.,-686.,1.797E+03)); +#4786 = LINE('',#4787,#4788); +#4787 = CARTESIAN_POINT('',(30.,-686.,1.797E+03)); +#4788 = VECTOR('',#4789,1.); +#4789 = DIRECTION('',(1.,0.,-0.)); +#4790 = ORIENTED_EDGE('',*,*,#4791,.T.); +#4791 = EDGE_CURVE('',#4782,#4792,#4794,.T.); +#4792 = VERTEX_POINT('',#4793); +#4793 = CARTESIAN_POINT('',(59.,-686.,1.303E+03)); +#4794 = LINE('',#4795,#4796); +#4795 = CARTESIAN_POINT('',(59.,-686.,679.)); +#4796 = VECTOR('',#4797,1.); +#4797 = DIRECTION('',(-0.,0.,-1.)); +#4798 = ORIENTED_EDGE('',*,*,#4799,.T.); +#4799 = EDGE_CURVE('',#4792,#4800,#4802,.T.); +#4800 = VERTEX_POINT('',#4801); +#4801 = CARTESIAN_POINT('',(268.,-686.,1.303E+03)); +#4802 = LINE('',#4803,#4804); +#4803 = CARTESIAN_POINT('',(30.,-686.,1.303E+03)); +#4804 = VECTOR('',#4805,1.); +#4805 = DIRECTION('',(1.,0.,-0.)); +#4806 = ORIENTED_EDGE('',*,*,#4807,.F.); +#4807 = EDGE_CURVE('',#4784,#4800,#4808,.T.); +#4808 = LINE('',#4809,#4810); +#4809 = CARTESIAN_POINT('',(268.,-686.,920.)); +#4810 = VECTOR('',#4811,1.); +#4811 = DIRECTION('',(-0.,0.,-1.)); +#4812 = FACE_BOUND('',#4813,.F.); +#4813 = EDGE_LOOP('',(#4814,#4824,#4832,#4840)); +#4814 = ORIENTED_EDGE('',*,*,#4815,.F.); +#4815 = EDGE_CURVE('',#4816,#4818,#4820,.T.); +#4816 = VERTEX_POINT('',#4817); +#4817 = CARTESIAN_POINT('',(315.,-686.,1.2286E+03)); +#4818 = VERTEX_POINT('',#4819); +#4819 = CARTESIAN_POINT('',(331.,-686.,1.2286E+03)); +#4820 = LINE('',#4821,#4822); +#4821 = CARTESIAN_POINT('',(158.,-686.,1.2286E+03)); +#4822 = VECTOR('',#4823,1.); +#4823 = DIRECTION('',(1.,0.,-0.)); +#4824 = ORIENTED_EDGE('',*,*,#4825,.T.); +#4825 = EDGE_CURVE('',#4816,#4826,#4828,.T.); +#4826 = VERTEX_POINT('',#4827); +#4827 = CARTESIAN_POINT('',(315.,-686.,1.1086E+03)); +#4828 = LINE('',#4829,#4830); +#4829 = CARTESIAN_POINT('',(315.,-686.,581.8)); +#4830 = VECTOR('',#4831,1.); +#4831 = DIRECTION('',(-0.,0.,-1.)); +#4832 = ORIENTED_EDGE('',*,*,#4833,.T.); +#4833 = EDGE_CURVE('',#4826,#4834,#4836,.T.); +#4834 = VERTEX_POINT('',#4835); +#4835 = CARTESIAN_POINT('',(331.,-686.,1.1086E+03)); +#4836 = LINE('',#4837,#4838); +#4837 = CARTESIAN_POINT('',(158.,-686.,1.1086E+03)); +#4838 = VECTOR('',#4839,1.); +#4839 = DIRECTION('',(1.,0.,-0.)); +#4840 = ORIENTED_EDGE('',*,*,#4841,.F.); +#4841 = EDGE_CURVE('',#4818,#4834,#4842,.T.); +#4842 = LINE('',#4843,#4844); +#4843 = CARTESIAN_POINT('',(331.,-686.,581.8)); +#4844 = VECTOR('',#4845,1.); +#4845 = DIRECTION('',(-0.,0.,-1.)); +#4846 = PLANE('',#4847); +#4847 = AXIS2_PLACEMENT_3D('',#4848,#4849,#4850); +#4848 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4849 = DIRECTION('',(-0.,1.,0.)); +#4850 = DIRECTION('',(0.,0.,1.)); +#4851 = ADVANCED_FACE('',(#4852),#4870,.T.); +#4852 = FACE_BOUND('',#4853,.T.); +#4853 = EDGE_LOOP('',(#4854,#4855,#4856,#4864)); +#4854 = ORIENTED_EDGE('',*,*,#4742,.F.); +#4855 = ORIENTED_EDGE('',*,*,#4765,.T.); +#4856 = ORIENTED_EDGE('',*,*,#4857,.T.); +#4857 = EDGE_CURVE('',#4766,#4858,#4860,.T.); +#4858 = VERTEX_POINT('',#4859); +#4859 = CARTESIAN_POINT('',(357.,-650.,2.25E+03)); +#4860 = LINE('',#4861,#4862); +#4861 = CARTESIAN_POINT('',(357.,-686.,2.25E+03)); +#4862 = VECTOR('',#4863,1.); +#4863 = DIRECTION('',(-0.,1.,0.)); +#4864 = ORIENTED_EDGE('',*,*,#4865,.F.); +#4865 = EDGE_CURVE('',#4735,#4858,#4866,.T.); +#4866 = LINE('',#4867,#4868); +#4867 = CARTESIAN_POINT('',(1.,-650.,2.25E+03)); +#4868 = VECTOR('',#4869,1.); +#4869 = DIRECTION('',(1.,0.,-0.)); +#4870 = PLANE('',#4871); +#4871 = AXIS2_PLACEMENT_3D('',#4872,#4873,#4874); +#4872 = CARTESIAN_POINT('',(1.,-686.,2.25E+03)); +#4873 = DIRECTION('',(0.,0.,1.)); +#4874 = DIRECTION('',(1.,0.,-0.)); +#4875 = ADVANCED_FACE('',(#4876,#4894),#4928,.T.); +#4876 = FACE_BOUND('',#4877,.T.); +#4877 = EDGE_LOOP('',(#4878,#4886,#4887,#4888)); +#4878 = ORIENTED_EDGE('',*,*,#4879,.F.); +#4879 = EDGE_CURVE('',#4727,#4880,#4882,.T.); +#4880 = VERTEX_POINT('',#4881); +#4881 = CARTESIAN_POINT('',(357.,-650.,55.)); +#4882 = LINE('',#4883,#4884); +#4883 = CARTESIAN_POINT('',(1.,-650.,55.)); +#4884 = VECTOR('',#4885,1.); +#4885 = DIRECTION('',(1.,0.,-0.)); +#4886 = ORIENTED_EDGE('',*,*,#4734,.T.); +#4887 = ORIENTED_EDGE('',*,*,#4865,.T.); +#4888 = ORIENTED_EDGE('',*,*,#4889,.F.); +#4889 = EDGE_CURVE('',#4880,#4858,#4890,.T.); +#4890 = LINE('',#4891,#4892); +#4891 = CARTESIAN_POINT('',(357.,-650.,55.)); +#4892 = VECTOR('',#4893,1.); +#4893 = DIRECTION('',(0.,0.,1.)); +#4894 = FACE_BOUND('',#4895,.T.); +#4895 = EDGE_LOOP('',(#4896,#4906,#4914,#4922)); +#4896 = ORIENTED_EDGE('',*,*,#4897,.F.); +#4897 = EDGE_CURVE('',#4898,#4900,#4902,.T.); +#4898 = VERTEX_POINT('',#4899); +#4899 = CARTESIAN_POINT('',(71.,-650.,1.785E+03)); +#4900 = VERTEX_POINT('',#4901); +#4901 = CARTESIAN_POINT('',(256.,-650.,1.785E+03)); +#4902 = LINE('',#4903,#4904); +#4903 = CARTESIAN_POINT('',(36.,-650.,1.785E+03)); +#4904 = VECTOR('',#4905,1.); +#4905 = DIRECTION('',(1.,0.,-0.)); +#4906 = ORIENTED_EDGE('',*,*,#4907,.T.); +#4907 = EDGE_CURVE('',#4898,#4908,#4910,.T.); +#4908 = VERTEX_POINT('',#4909); +#4909 = CARTESIAN_POINT('',(71.,-650.,1.315E+03)); +#4910 = LINE('',#4911,#4912); +#4911 = CARTESIAN_POINT('',(71.,-650.,685.)); +#4912 = VECTOR('',#4913,1.); +#4913 = DIRECTION('',(-0.,0.,-1.)); +#4914 = ORIENTED_EDGE('',*,*,#4915,.T.); +#4915 = EDGE_CURVE('',#4908,#4916,#4918,.T.); +#4916 = VERTEX_POINT('',#4917); +#4917 = CARTESIAN_POINT('',(256.,-650.,1.315E+03)); +#4918 = LINE('',#4919,#4920); +#4919 = CARTESIAN_POINT('',(36.,-650.,1.315E+03)); +#4920 = VECTOR('',#4921,1.); +#4921 = DIRECTION('',(1.,0.,-0.)); +#4922 = ORIENTED_EDGE('',*,*,#4923,.F.); +#4923 = EDGE_CURVE('',#4900,#4916,#4924,.T.); +#4924 = LINE('',#4925,#4926); +#4925 = CARTESIAN_POINT('',(256.,-650.,685.)); +#4926 = VECTOR('',#4927,1.); +#4927 = DIRECTION('',(-0.,0.,-1.)); +#4928 = PLANE('',#4929); +#4929 = AXIS2_PLACEMENT_3D('',#4930,#4931,#4932); +#4930 = CARTESIAN_POINT('',(1.,-650.,55.)); +#4931 = DIRECTION('',(-0.,1.,0.)); +#4932 = DIRECTION('',(0.,0.,1.)); +#4933 = ADVANCED_FACE('',(#4934),#4945,.F.); +#4934 = FACE_BOUND('',#4935,.F.); +#4935 = EDGE_LOOP('',(#4936,#4937,#4938,#4944)); +#4936 = ORIENTED_EDGE('',*,*,#4726,.F.); +#4937 = ORIENTED_EDGE('',*,*,#4756,.T.); +#4938 = ORIENTED_EDGE('',*,*,#4939,.T.); +#4939 = EDGE_CURVE('',#4757,#4880,#4940,.T.); +#4940 = LINE('',#4941,#4942); +#4941 = CARTESIAN_POINT('',(357.,-686.,55.)); +#4942 = VECTOR('',#4943,1.); +#4943 = DIRECTION('',(-0.,1.,0.)); +#4944 = ORIENTED_EDGE('',*,*,#4879,.F.); +#4945 = PLANE('',#4946); +#4946 = AXIS2_PLACEMENT_3D('',#4947,#4948,#4949); +#4947 = CARTESIAN_POINT('',(1.,-686.,55.)); +#4948 = DIRECTION('',(0.,0.,1.)); +#4949 = DIRECTION('',(1.,0.,-0.)); +#4950 = ADVANCED_FACE('',(#4951),#4957,.T.); +#4951 = FACE_BOUND('',#4952,.T.); +#4952 = EDGE_LOOP('',(#4953,#4954,#4955,#4956)); +#4953 = ORIENTED_EDGE('',*,*,#4773,.F.); +#4954 = ORIENTED_EDGE('',*,*,#4939,.T.); +#4955 = ORIENTED_EDGE('',*,*,#4889,.T.); +#4956 = ORIENTED_EDGE('',*,*,#4857,.F.); +#4957 = PLANE('',#4958); +#4958 = AXIS2_PLACEMENT_3D('',#4959,#4960,#4961); +#4959 = CARTESIAN_POINT('',(357.,-686.,55.)); +#4960 = DIRECTION('',(1.,0.,-0.)); +#4961 = DIRECTION('',(0.,0.,1.)); +#4962 = ADVANCED_FACE('',(#4963),#4988,.T.); +#4963 = FACE_BOUND('',#4964,.T.); +#4964 = EDGE_LOOP('',(#4965,#4973,#4981,#4987)); +#4965 = ORIENTED_EDGE('',*,*,#4966,.F.); +#4966 = EDGE_CURVE('',#4967,#4782,#4969,.T.); +#4967 = VERTEX_POINT('',#4968); +#4968 = CARTESIAN_POINT('',(59.,-690.,1.797E+03)); +#4969 = LINE('',#4970,#4971); +#4970 = CARTESIAN_POINT('',(59.,-690.,1.797E+03)); +#4971 = VECTOR('',#4972,1.); +#4972 = DIRECTION('',(-0.,1.,0.)); +#4973 = ORIENTED_EDGE('',*,*,#4974,.T.); +#4974 = EDGE_CURVE('',#4967,#4975,#4977,.T.); +#4975 = VERTEX_POINT('',#4976); +#4976 = CARTESIAN_POINT('',(268.,-690.,1.797E+03)); +#4977 = LINE('',#4978,#4979); +#4978 = CARTESIAN_POINT('',(59.,-690.,1.797E+03)); +#4979 = VECTOR('',#4980,1.); +#4980 = DIRECTION('',(1.,0.,-0.)); +#4981 = ORIENTED_EDGE('',*,*,#4982,.T.); +#4982 = EDGE_CURVE('',#4975,#4784,#4983,.T.); +#4983 = LINE('',#4984,#4985); +#4984 = CARTESIAN_POINT('',(268.,-690.,1.797E+03)); +#4985 = VECTOR('',#4986,1.); +#4986 = DIRECTION('',(-0.,1.,0.)); +#4987 = ORIENTED_EDGE('',*,*,#4781,.F.); +#4988 = PLANE('',#4989); +#4989 = AXIS2_PLACEMENT_3D('',#4990,#4991,#4992); +#4990 = CARTESIAN_POINT('',(59.,-690.,1.797E+03)); +#4991 = DIRECTION('',(0.,0.,1.)); +#4992 = DIRECTION('',(1.,0.,-0.)); +#4993 = ADVANCED_FACE('',(#4994),#5019,.F.); +#4994 = FACE_BOUND('',#4995,.F.); +#4995 = EDGE_LOOP('',(#4996,#5004,#5012,#5018)); +#4996 = ORIENTED_EDGE('',*,*,#4997,.F.); +#4997 = EDGE_CURVE('',#4998,#4792,#5000,.T.); +#4998 = VERTEX_POINT('',#4999); +#4999 = CARTESIAN_POINT('',(59.,-690.,1.303E+03)); +#5000 = LINE('',#5001,#5002); +#5001 = CARTESIAN_POINT('',(59.,-690.,1.303E+03)); +#5002 = VECTOR('',#5003,1.); +#5003 = DIRECTION('',(-0.,1.,0.)); +#5004 = ORIENTED_EDGE('',*,*,#5005,.T.); +#5005 = EDGE_CURVE('',#4998,#5006,#5008,.T.); +#5006 = VERTEX_POINT('',#5007); +#5007 = CARTESIAN_POINT('',(268.,-690.,1.303E+03)); +#5008 = LINE('',#5009,#5010); +#5009 = CARTESIAN_POINT('',(59.,-690.,1.303E+03)); +#5010 = VECTOR('',#5011,1.); +#5011 = DIRECTION('',(1.,0.,-0.)); +#5012 = ORIENTED_EDGE('',*,*,#5013,.T.); +#5013 = EDGE_CURVE('',#5006,#4800,#5014,.T.); +#5014 = LINE('',#5015,#5016); +#5015 = CARTESIAN_POINT('',(268.,-690.,1.303E+03)); +#5016 = VECTOR('',#5017,1.); +#5017 = DIRECTION('',(-0.,1.,0.)); +#5018 = ORIENTED_EDGE('',*,*,#4799,.F.); +#5019 = PLANE('',#5020); +#5020 = AXIS2_PLACEMENT_3D('',#5021,#5022,#5023); +#5021 = CARTESIAN_POINT('',(59.,-690.,1.303E+03)); +#5022 = DIRECTION('',(0.,0.,1.)); +#5023 = DIRECTION('',(1.,0.,-0.)); +#5024 = ADVANCED_FACE('',(#5025),#5050,.T.); +#5025 = FACE_BOUND('',#5026,.T.); +#5026 = EDGE_LOOP('',(#5027,#5035,#5043,#5049)); +#5027 = ORIENTED_EDGE('',*,*,#5028,.F.); +#5028 = EDGE_CURVE('',#5029,#4816,#5031,.T.); +#5029 = VERTEX_POINT('',#5030); +#5030 = CARTESIAN_POINT('',(315.,-698.,1.2286E+03)); +#5031 = LINE('',#5032,#5033); +#5032 = CARTESIAN_POINT('',(315.,-698.,1.2286E+03)); +#5033 = VECTOR('',#5034,1.); +#5034 = DIRECTION('',(-0.,1.,0.)); +#5035 = ORIENTED_EDGE('',*,*,#5036,.T.); +#5036 = EDGE_CURVE('',#5029,#5037,#5039,.T.); +#5037 = VERTEX_POINT('',#5038); +#5038 = CARTESIAN_POINT('',(331.,-698.,1.2286E+03)); +#5039 = LINE('',#5040,#5041); +#5040 = CARTESIAN_POINT('',(315.,-698.,1.2286E+03)); +#5041 = VECTOR('',#5042,1.); +#5042 = DIRECTION('',(1.,0.,-0.)); +#5043 = ORIENTED_EDGE('',*,*,#5044,.T.); +#5044 = EDGE_CURVE('',#5037,#4818,#5045,.T.); +#5045 = LINE('',#5046,#5047); +#5046 = CARTESIAN_POINT('',(331.,-698.,1.2286E+03)); +#5047 = VECTOR('',#5048,1.); +#5048 = DIRECTION('',(-0.,1.,0.)); +#5049 = ORIENTED_EDGE('',*,*,#4815,.F.); +#5050 = PLANE('',#5051); +#5051 = AXIS2_PLACEMENT_3D('',#5052,#5053,#5054); +#5052 = CARTESIAN_POINT('',(315.,-698.,1.2286E+03)); +#5053 = DIRECTION('',(0.,0.,1.)); +#5054 = DIRECTION('',(1.,0.,-0.)); +#5055 = ADVANCED_FACE('',(#5056),#5074,.F.); +#5056 = FACE_BOUND('',#5057,.F.); +#5057 = EDGE_LOOP('',(#5058,#5059,#5067,#5073)); +#5058 = ORIENTED_EDGE('',*,*,#5028,.F.); +#5059 = ORIENTED_EDGE('',*,*,#5060,.F.); +#5060 = EDGE_CURVE('',#5061,#5029,#5063,.T.); +#5061 = VERTEX_POINT('',#5062); +#5062 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5063 = LINE('',#5064,#5065); +#5064 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5065 = VECTOR('',#5066,1.); +#5066 = DIRECTION('',(0.,0.,1.)); +#5067 = ORIENTED_EDGE('',*,*,#5068,.T.); +#5068 = EDGE_CURVE('',#5061,#4826,#5069,.T.); +#5069 = LINE('',#5070,#5071); +#5070 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5071 = VECTOR('',#5072,1.); +#5072 = DIRECTION('',(-0.,1.,0.)); +#5073 = ORIENTED_EDGE('',*,*,#4825,.F.); +#5074 = PLANE('',#5075); +#5075 = AXIS2_PLACEMENT_3D('',#5076,#5077,#5078); +#5076 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5077 = DIRECTION('',(1.,0.,-0.)); +#5078 = DIRECTION('',(0.,0.,1.)); +#5079 = ADVANCED_FACE('',(#5080),#5098,.T.); +#5080 = FACE_BOUND('',#5081,.T.); +#5081 = EDGE_LOOP('',(#5082,#5083,#5091,#5097)); +#5082 = ORIENTED_EDGE('',*,*,#5044,.F.); +#5083 = ORIENTED_EDGE('',*,*,#5084,.F.); +#5084 = EDGE_CURVE('',#5085,#5037,#5087,.T.); +#5085 = VERTEX_POINT('',#5086); +#5086 = CARTESIAN_POINT('',(331.,-698.,1.1086E+03)); +#5087 = LINE('',#5088,#5089); +#5088 = CARTESIAN_POINT('',(331.,-698.,1.1086E+03)); +#5089 = VECTOR('',#5090,1.); +#5090 = DIRECTION('',(0.,0.,1.)); +#5091 = ORIENTED_EDGE('',*,*,#5092,.T.); +#5092 = EDGE_CURVE('',#5085,#4834,#5093,.T.); +#5093 = LINE('',#5094,#5095); +#5094 = CARTESIAN_POINT('',(331.,-698.,1.1086E+03)); +#5095 = VECTOR('',#5096,1.); +#5096 = DIRECTION('',(-0.,1.,0.)); +#5097 = ORIENTED_EDGE('',*,*,#4841,.F.); +#5098 = PLANE('',#5099); +#5099 = AXIS2_PLACEMENT_3D('',#5100,#5101,#5102); +#5100 = CARTESIAN_POINT('',(331.,-698.,1.1086E+03)); +#5101 = DIRECTION('',(1.,0.,-0.)); +#5102 = DIRECTION('',(0.,0.,1.)); +#5103 = ADVANCED_FACE('',(#5104),#5115,.F.); +#5104 = FACE_BOUND('',#5105,.F.); +#5105 = EDGE_LOOP('',(#5106,#5107,#5113,#5114)); +#5106 = ORIENTED_EDGE('',*,*,#5068,.F.); +#5107 = ORIENTED_EDGE('',*,*,#5108,.T.); +#5108 = EDGE_CURVE('',#5061,#5085,#5109,.T.); +#5109 = LINE('',#5110,#5111); +#5110 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5111 = VECTOR('',#5112,1.); +#5112 = DIRECTION('',(1.,0.,-0.)); +#5113 = ORIENTED_EDGE('',*,*,#5092,.T.); +#5114 = ORIENTED_EDGE('',*,*,#4833,.F.); +#5115 = PLANE('',#5116); +#5116 = AXIS2_PLACEMENT_3D('',#5117,#5118,#5119); +#5117 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5118 = DIRECTION('',(0.,0.,1.)); +#5119 = DIRECTION('',(1.,0.,-0.)); +#5120 = ADVANCED_FACE('',(#5121),#5127,.F.); +#5121 = FACE_BOUND('',#5122,.F.); +#5122 = EDGE_LOOP('',(#5123,#5124,#5125,#5126)); +#5123 = ORIENTED_EDGE('',*,*,#5108,.F.); +#5124 = ORIENTED_EDGE('',*,*,#5060,.T.); +#5125 = ORIENTED_EDGE('',*,*,#5036,.T.); +#5126 = ORIENTED_EDGE('',*,*,#5084,.F.); +#5127 = PLANE('',#5128); +#5128 = AXIS2_PLACEMENT_3D('',#5129,#5130,#5131); +#5129 = CARTESIAN_POINT('',(315.,-698.,1.1086E+03)); +#5130 = DIRECTION('',(-0.,1.,0.)); +#5131 = DIRECTION('',(0.,0.,1.)); +#5132 = ADVANCED_FACE('',(#5133),#5144,.T.); +#5133 = FACE_BOUND('',#5134,.T.); +#5134 = EDGE_LOOP('',(#5135,#5136,#5142,#5143)); +#5135 = ORIENTED_EDGE('',*,*,#4982,.F.); +#5136 = ORIENTED_EDGE('',*,*,#5137,.F.); +#5137 = EDGE_CURVE('',#5006,#4975,#5138,.T.); +#5138 = LINE('',#5139,#5140); +#5139 = CARTESIAN_POINT('',(268.,-690.,1.785E+03)); +#5140 = VECTOR('',#5141,1.); +#5141 = DIRECTION('',(0.,0.,1.)); +#5142 = ORIENTED_EDGE('',*,*,#5013,.T.); +#5143 = ORIENTED_EDGE('',*,*,#4807,.F.); +#5144 = PLANE('',#5145); +#5145 = AXIS2_PLACEMENT_3D('',#5146,#5147,#5148); +#5146 = CARTESIAN_POINT('',(268.,-688.,1.55E+03)); +#5147 = DIRECTION('',(1.,0.,0.)); +#5148 = DIRECTION('',(-0.,0.,1.)); +#5149 = ADVANCED_FACE('',(#5150),#5161,.T.); +#5150 = FACE_BOUND('',#5151,.T.); +#5151 = EDGE_LOOP('',(#5152,#5153,#5154,#5160)); +#5152 = ORIENTED_EDGE('',*,*,#4791,.T.); +#5153 = ORIENTED_EDGE('',*,*,#4997,.F.); +#5154 = ORIENTED_EDGE('',*,*,#5155,.T.); +#5155 = EDGE_CURVE('',#4998,#4967,#5156,.T.); +#5156 = LINE('',#5157,#5158); +#5157 = CARTESIAN_POINT('',(59.,-690.,1.303E+03)); +#5158 = VECTOR('',#5159,1.); +#5159 = DIRECTION('',(0.,0.,1.)); +#5160 = ORIENTED_EDGE('',*,*,#4966,.T.); +#5161 = PLANE('',#5162); +#5162 = AXIS2_PLACEMENT_3D('',#5163,#5164,#5165); +#5163 = CARTESIAN_POINT('',(59.,-688.,1.55E+03)); +#5164 = DIRECTION('',(-1.,-0.,-0.)); +#5165 = DIRECTION('',(0.,0.,-1.)); +#5166 = ADVANCED_FACE('',(#5167),#5192,.T.); +#5167 = FACE_BOUND('',#5168,.T.); +#5168 = EDGE_LOOP('',(#5169,#5170,#5178,#5186)); +#5169 = ORIENTED_EDGE('',*,*,#4897,.T.); +#5170 = ORIENTED_EDGE('',*,*,#5171,.F.); +#5171 = EDGE_CURVE('',#5172,#4900,#5174,.T.); +#5172 = VERTEX_POINT('',#5173); +#5173 = CARTESIAN_POINT('',(256.,-690.,1.785E+03)); +#5174 = LINE('',#5175,#5176); +#5175 = CARTESIAN_POINT('',(256.,-689.,1.785E+03)); +#5176 = VECTOR('',#5177,1.); +#5177 = DIRECTION('',(-0.,1.,0.)); +#5178 = ORIENTED_EDGE('',*,*,#5179,.F.); +#5179 = EDGE_CURVE('',#5180,#5172,#5182,.T.); +#5180 = VERTEX_POINT('',#5181); +#5181 = CARTESIAN_POINT('',(71.,-690.,1.785E+03)); +#5182 = LINE('',#5183,#5184); +#5183 = CARTESIAN_POINT('',(59.,-690.,1.785E+03)); +#5184 = VECTOR('',#5185,1.); +#5185 = DIRECTION('',(1.,0.,-0.)); +#5186 = ORIENTED_EDGE('',*,*,#5187,.T.); +#5187 = EDGE_CURVE('',#5180,#4898,#5188,.T.); +#5188 = LINE('',#5189,#5190); +#5189 = CARTESIAN_POINT('',(71.,-690.,1.785E+03)); +#5190 = VECTOR('',#5191,1.); +#5191 = DIRECTION('',(-0.,1.,0.)); +#5192 = PLANE('',#5193); +#5193 = AXIS2_PLACEMENT_3D('',#5194,#5195,#5196); +#5194 = CARTESIAN_POINT('',(163.5,-670.,1.785E+03)); +#5195 = DIRECTION('',(-0.,-0.,-1.)); +#5196 = DIRECTION('',(-1.,0.,0.)); +#5197 = ADVANCED_FACE('',(#5198),#5216,.T.); +#5198 = FACE_BOUND('',#5199,.T.); +#5199 = EDGE_LOOP('',(#5200,#5201,#5202,#5210)); +#5200 = ORIENTED_EDGE('',*,*,#5171,.T.); +#5201 = ORIENTED_EDGE('',*,*,#4923,.T.); +#5202 = ORIENTED_EDGE('',*,*,#5203,.F.); +#5203 = EDGE_CURVE('',#5204,#4916,#5206,.T.); +#5204 = VERTEX_POINT('',#5205); +#5205 = CARTESIAN_POINT('',(256.,-690.,1.315E+03)); +#5206 = LINE('',#5207,#5208); +#5207 = CARTESIAN_POINT('',(256.,-689.,1.315E+03)); +#5208 = VECTOR('',#5209,1.); +#5209 = DIRECTION('',(-0.,1.,0.)); +#5210 = ORIENTED_EDGE('',*,*,#5211,.T.); +#5211 = EDGE_CURVE('',#5204,#5172,#5212,.T.); +#5212 = LINE('',#5213,#5214); +#5213 = CARTESIAN_POINT('',(256.,-690.,1.315E+03)); +#5214 = VECTOR('',#5215,1.); +#5215 = DIRECTION('',(0.,0.,1.)); +#5216 = PLANE('',#5217); +#5217 = AXIS2_PLACEMENT_3D('',#5218,#5219,#5220); +#5218 = CARTESIAN_POINT('',(256.,-670.,1.55E+03)); +#5219 = DIRECTION('',(-1.,-0.,-0.)); +#5220 = DIRECTION('',(0.,0.,-1.)); +#5221 = ADVANCED_FACE('',(#5222),#5240,.T.); +#5222 = FACE_BOUND('',#5223,.T.); +#5223 = EDGE_LOOP('',(#5224,#5232,#5238,#5239)); +#5224 = ORIENTED_EDGE('',*,*,#5225,.F.); +#5225 = EDGE_CURVE('',#5226,#4908,#5228,.T.); +#5226 = VERTEX_POINT('',#5227); +#5227 = CARTESIAN_POINT('',(71.,-690.,1.315E+03)); +#5228 = LINE('',#5229,#5230); +#5229 = CARTESIAN_POINT('',(71.,-689.,1.315E+03)); +#5230 = VECTOR('',#5231,1.); +#5231 = DIRECTION('',(-0.,1.,0.)); +#5232 = ORIENTED_EDGE('',*,*,#5233,.T.); +#5233 = EDGE_CURVE('',#5226,#5204,#5234,.T.); +#5234 = LINE('',#5235,#5236); +#5235 = CARTESIAN_POINT('',(59.,-690.,1.315E+03)); +#5236 = VECTOR('',#5237,1.); +#5237 = DIRECTION('',(1.,0.,-0.)); +#5238 = ORIENTED_EDGE('',*,*,#5203,.T.); +#5239 = ORIENTED_EDGE('',*,*,#4915,.F.); +#5240 = PLANE('',#5241); +#5241 = AXIS2_PLACEMENT_3D('',#5242,#5243,#5244); +#5242 = CARTESIAN_POINT('',(163.5,-670.,1.315E+03)); +#5243 = DIRECTION('',(0.,0.,1.)); +#5244 = DIRECTION('',(1.,0.,-0.)); +#5245 = ADVANCED_FACE('',(#5246),#5257,.T.); +#5246 = FACE_BOUND('',#5247,.T.); +#5247 = EDGE_LOOP('',(#5248,#5249,#5255,#5256)); +#5248 = ORIENTED_EDGE('',*,*,#5187,.F.); +#5249 = ORIENTED_EDGE('',*,*,#5250,.F.); +#5250 = EDGE_CURVE('',#5226,#5180,#5251,.T.); +#5251 = LINE('',#5252,#5253); +#5252 = CARTESIAN_POINT('',(71.,-690.,1.315E+03)); +#5253 = VECTOR('',#5254,1.); +#5254 = DIRECTION('',(0.,0.,1.)); +#5255 = ORIENTED_EDGE('',*,*,#5225,.T.); +#5256 = ORIENTED_EDGE('',*,*,#4907,.F.); +#5257 = PLANE('',#5258); +#5258 = AXIS2_PLACEMENT_3D('',#5259,#5260,#5261); +#5259 = CARTESIAN_POINT('',(71.,-670.,1.55E+03)); +#5260 = DIRECTION('',(1.,0.,0.)); +#5261 = DIRECTION('',(-0.,0.,1.)); +#5262 = ADVANCED_FACE('',(#5263,#5269),#5275,.T.); +#5263 = FACE_BOUND('',#5264,.T.); +#5264 = EDGE_LOOP('',(#5265,#5266,#5267,#5268)); +#5265 = ORIENTED_EDGE('',*,*,#5137,.T.); +#5266 = ORIENTED_EDGE('',*,*,#4974,.F.); +#5267 = ORIENTED_EDGE('',*,*,#5155,.F.); +#5268 = ORIENTED_EDGE('',*,*,#5005,.T.); +#5269 = FACE_BOUND('',#5270,.T.); +#5270 = EDGE_LOOP('',(#5271,#5272,#5273,#5274)); +#5271 = ORIENTED_EDGE('',*,*,#5179,.T.); +#5272 = ORIENTED_EDGE('',*,*,#5211,.F.); +#5273 = ORIENTED_EDGE('',*,*,#5233,.F.); +#5274 = ORIENTED_EDGE('',*,*,#5250,.T.); +#5275 = PLANE('',#5276); +#5276 = AXIS2_PLACEMENT_3D('',#5277,#5278,#5279); +#5277 = CARTESIAN_POINT('',(163.5,-690.,1.55E+03)); +#5278 = DIRECTION('',(-0.,-1.,-0.)); +#5279 = DIRECTION('',(0.,0.,-1.)); +#5280 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5284)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#5281,#5282,#5283)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#5281 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#5282 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#5283 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#5284 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5281, + 'distance_accuracy_value','confusion accuracy'); +#5285 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5286,#5288); +#5286 = ( REPRESENTATION_RELATIONSHIP('','',#4709,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5287) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#5287 = ITEM_DEFINED_TRANSFORMATION('','',#11,#35); +#5288 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #5289); +#5289 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('6','NAU03_Front_Right_Door','', + #5,#4704,$); +#5290 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4706)); +#5291 = SHAPE_DEFINITION_REPRESENTATION(#5292,#5298); +#5292 = PRODUCT_DEFINITION_SHAPE('','',#5293); +#5293 = PRODUCT_DEFINITION('design','',#5294,#5297); +#5294 = PRODUCT_DEFINITION_FORMATION('','',#5295); +#5295 = PRODUCT('NAU03_Top_Roof','NAU03_Top_Roof','',(#5296)); +#5296 = PRODUCT_CONTEXT('',#2,'mechanical'); +#5297 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#5298 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5299),#5733); +#5299 = MANIFOLD_SOLID_BREP('',#5300); +#5300 = CLOSED_SHELL('',(#5301,#5341,#5372,#5396,#5420,#5505,#5517,#5548 + ,#5572,#5596,#5613,#5644,#5668,#5692,#5709,#5721)); +#5301 = ADVANCED_FACE('',(#5302),#5336,.F.); +#5302 = FACE_BOUND('',#5303,.F.); +#5303 = EDGE_LOOP('',(#5304,#5314,#5322,#5330)); +#5304 = ORIENTED_EDGE('',*,*,#5305,.F.); +#5305 = EDGE_CURVE('',#5306,#5308,#5310,.T.); +#5306 = VERTEX_POINT('',#5307); +#5307 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5308 = VERTEX_POINT('',#5309); +#5309 = CARTESIAN_POINT('',(-410.,-700.,2.375E+03)); +#5310 = LINE('',#5311,#5312); +#5311 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5312 = VECTOR('',#5313,1.); +#5313 = DIRECTION('',(0.,0.,1.)); +#5314 = ORIENTED_EDGE('',*,*,#5315,.T.); +#5315 = EDGE_CURVE('',#5306,#5316,#5318,.T.); +#5316 = VERTEX_POINT('',#5317); +#5317 = CARTESIAN_POINT('',(-410.,700.,2.3E+03)); +#5318 = LINE('',#5319,#5320); +#5319 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5320 = VECTOR('',#5321,1.); +#5321 = DIRECTION('',(-0.,1.,0.)); +#5322 = ORIENTED_EDGE('',*,*,#5323,.T.); +#5323 = EDGE_CURVE('',#5316,#5324,#5326,.T.); +#5324 = VERTEX_POINT('',#5325); +#5325 = CARTESIAN_POINT('',(-410.,700.,2.375E+03)); +#5326 = LINE('',#5327,#5328); +#5327 = CARTESIAN_POINT('',(-410.,700.,2.3E+03)); +#5328 = VECTOR('',#5329,1.); +#5329 = DIRECTION('',(0.,0.,1.)); +#5330 = ORIENTED_EDGE('',*,*,#5331,.F.); +#5331 = EDGE_CURVE('',#5308,#5324,#5332,.T.); +#5332 = LINE('',#5333,#5334); +#5333 = CARTESIAN_POINT('',(-410.,-700.,2.375E+03)); +#5334 = VECTOR('',#5335,1.); +#5335 = DIRECTION('',(-0.,1.,0.)); +#5336 = PLANE('',#5337); +#5337 = AXIS2_PLACEMENT_3D('',#5338,#5339,#5340); +#5338 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5339 = DIRECTION('',(1.,0.,-0.)); +#5340 = DIRECTION('',(0.,0.,1.)); +#5341 = ADVANCED_FACE('',(#5342),#5367,.F.); +#5342 = FACE_BOUND('',#5343,.F.); +#5343 = EDGE_LOOP('',(#5344,#5352,#5353,#5361)); +#5344 = ORIENTED_EDGE('',*,*,#5345,.F.); +#5345 = EDGE_CURVE('',#5306,#5346,#5348,.T.); +#5346 = VERTEX_POINT('',#5347); +#5347 = CARTESIAN_POINT('',(410.,-700.,2.3E+03)); +#5348 = LINE('',#5349,#5350); +#5349 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5350 = VECTOR('',#5351,1.); +#5351 = DIRECTION('',(1.,0.,-0.)); +#5352 = ORIENTED_EDGE('',*,*,#5305,.T.); +#5353 = ORIENTED_EDGE('',*,*,#5354,.T.); +#5354 = EDGE_CURVE('',#5308,#5355,#5357,.T.); +#5355 = VERTEX_POINT('',#5356); +#5356 = CARTESIAN_POINT('',(410.,-700.,2.375E+03)); +#5357 = LINE('',#5358,#5359); +#5358 = CARTESIAN_POINT('',(-410.,-700.,2.375E+03)); +#5359 = VECTOR('',#5360,1.); +#5360 = DIRECTION('',(1.,0.,-0.)); +#5361 = ORIENTED_EDGE('',*,*,#5362,.F.); +#5362 = EDGE_CURVE('',#5346,#5355,#5363,.T.); +#5363 = LINE('',#5364,#5365); +#5364 = CARTESIAN_POINT('',(410.,-700.,2.3E+03)); +#5365 = VECTOR('',#5366,1.); +#5366 = DIRECTION('',(0.,0.,1.)); +#5367 = PLANE('',#5368); +#5368 = AXIS2_PLACEMENT_3D('',#5369,#5370,#5371); +#5369 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5370 = DIRECTION('',(-0.,1.,0.)); +#5371 = DIRECTION('',(0.,0.,1.)); +#5372 = ADVANCED_FACE('',(#5373),#5391,.T.); +#5373 = FACE_BOUND('',#5374,.T.); +#5374 = EDGE_LOOP('',(#5375,#5376,#5377,#5385)); +#5375 = ORIENTED_EDGE('',*,*,#5331,.F.); +#5376 = ORIENTED_EDGE('',*,*,#5354,.T.); +#5377 = ORIENTED_EDGE('',*,*,#5378,.T.); +#5378 = EDGE_CURVE('',#5355,#5379,#5381,.T.); +#5379 = VERTEX_POINT('',#5380); +#5380 = CARTESIAN_POINT('',(410.,700.,2.375E+03)); +#5381 = LINE('',#5382,#5383); +#5382 = CARTESIAN_POINT('',(410.,-700.,2.375E+03)); +#5383 = VECTOR('',#5384,1.); +#5384 = DIRECTION('',(-0.,1.,0.)); +#5385 = ORIENTED_EDGE('',*,*,#5386,.F.); +#5386 = EDGE_CURVE('',#5324,#5379,#5387,.T.); +#5387 = LINE('',#5388,#5389); +#5388 = CARTESIAN_POINT('',(-410.,700.,2.375E+03)); +#5389 = VECTOR('',#5390,1.); +#5390 = DIRECTION('',(1.,0.,-0.)); +#5391 = PLANE('',#5392); +#5392 = AXIS2_PLACEMENT_3D('',#5393,#5394,#5395); +#5393 = CARTESIAN_POINT('',(-410.,-700.,2.375E+03)); +#5394 = DIRECTION('',(0.,0.,1.)); +#5395 = DIRECTION('',(1.,0.,-0.)); +#5396 = ADVANCED_FACE('',(#5397),#5415,.T.); +#5397 = FACE_BOUND('',#5398,.T.); +#5398 = EDGE_LOOP('',(#5399,#5407,#5408,#5409)); +#5399 = ORIENTED_EDGE('',*,*,#5400,.F.); +#5400 = EDGE_CURVE('',#5316,#5401,#5403,.T.); +#5401 = VERTEX_POINT('',#5402); +#5402 = CARTESIAN_POINT('',(410.,700.,2.3E+03)); +#5403 = LINE('',#5404,#5405); +#5404 = CARTESIAN_POINT('',(-410.,700.,2.3E+03)); +#5405 = VECTOR('',#5406,1.); +#5406 = DIRECTION('',(1.,0.,-0.)); +#5407 = ORIENTED_EDGE('',*,*,#5323,.T.); +#5408 = ORIENTED_EDGE('',*,*,#5386,.T.); +#5409 = ORIENTED_EDGE('',*,*,#5410,.F.); +#5410 = EDGE_CURVE('',#5401,#5379,#5411,.T.); +#5411 = LINE('',#5412,#5413); +#5412 = CARTESIAN_POINT('',(410.,700.,2.3E+03)); +#5413 = VECTOR('',#5414,1.); +#5414 = DIRECTION('',(0.,0.,1.)); +#5415 = PLANE('',#5416); +#5416 = AXIS2_PLACEMENT_3D('',#5417,#5418,#5419); +#5417 = CARTESIAN_POINT('',(-410.,700.,2.3E+03)); +#5418 = DIRECTION('',(-0.,1.,0.)); +#5419 = DIRECTION('',(0.,0.,1.)); +#5420 = ADVANCED_FACE('',(#5421,#5432,#5466),#5500,.F.); +#5421 = FACE_BOUND('',#5422,.F.); +#5422 = EDGE_LOOP('',(#5423,#5424,#5425,#5431)); +#5423 = ORIENTED_EDGE('',*,*,#5315,.F.); +#5424 = ORIENTED_EDGE('',*,*,#5345,.T.); +#5425 = ORIENTED_EDGE('',*,*,#5426,.T.); +#5426 = EDGE_CURVE('',#5346,#5401,#5427,.T.); +#5427 = LINE('',#5428,#5429); +#5428 = CARTESIAN_POINT('',(410.,-700.,2.3E+03)); +#5429 = VECTOR('',#5430,1.); +#5430 = DIRECTION('',(-0.,1.,0.)); +#5431 = ORIENTED_EDGE('',*,*,#5400,.F.); +#5432 = FACE_BOUND('',#5433,.F.); +#5433 = EDGE_LOOP('',(#5434,#5444,#5452,#5460)); +#5434 = ORIENTED_EDGE('',*,*,#5435,.F.); +#5435 = EDGE_CURVE('',#5436,#5438,#5440,.T.); +#5436 = VERTEX_POINT('',#5437); +#5437 = CARTESIAN_POINT('',(-390.,-660.,2.3E+03)); +#5438 = VERTEX_POINT('',#5439); +#5439 = CARTESIAN_POINT('',(390.,-660.,2.3E+03)); +#5440 = LINE('',#5441,#5442); +#5441 = CARTESIAN_POINT('',(-390.,-660.,2.3E+03)); +#5442 = VECTOR('',#5443,1.); +#5443 = DIRECTION('',(1.,0.,-0.)); +#5444 = ORIENTED_EDGE('',*,*,#5445,.T.); +#5445 = EDGE_CURVE('',#5436,#5446,#5448,.T.); +#5446 = VERTEX_POINT('',#5447); +#5447 = CARTESIAN_POINT('',(-390.,-602.,2.3E+03)); +#5448 = LINE('',#5449,#5450); +#5449 = CARTESIAN_POINT('',(-390.,-660.,2.3E+03)); +#5450 = VECTOR('',#5451,1.); +#5451 = DIRECTION('',(-0.,1.,0.)); +#5452 = ORIENTED_EDGE('',*,*,#5453,.T.); +#5453 = EDGE_CURVE('',#5446,#5454,#5456,.T.); +#5454 = VERTEX_POINT('',#5455); +#5455 = CARTESIAN_POINT('',(390.,-602.,2.3E+03)); +#5456 = LINE('',#5457,#5458); +#5457 = CARTESIAN_POINT('',(-390.,-602.,2.3E+03)); +#5458 = VECTOR('',#5459,1.); +#5459 = DIRECTION('',(1.,0.,-0.)); +#5460 = ORIENTED_EDGE('',*,*,#5461,.F.); +#5461 = EDGE_CURVE('',#5438,#5454,#5462,.T.); +#5462 = LINE('',#5463,#5464); +#5463 = CARTESIAN_POINT('',(390.,-660.,2.3E+03)); +#5464 = VECTOR('',#5465,1.); +#5465 = DIRECTION('',(-0.,1.,0.)); +#5466 = FACE_BOUND('',#5467,.F.); +#5467 = EDGE_LOOP('',(#5468,#5478,#5486,#5494)); +#5468 = ORIENTED_EDGE('',*,*,#5469,.F.); +#5469 = EDGE_CURVE('',#5470,#5472,#5474,.T.); +#5470 = VERTEX_POINT('',#5471); +#5471 = CARTESIAN_POINT('',(-390.,602.,2.3E+03)); +#5472 = VERTEX_POINT('',#5473); +#5473 = CARTESIAN_POINT('',(390.,602.,2.3E+03)); +#5474 = LINE('',#5475,#5476); +#5475 = CARTESIAN_POINT('',(-390.,602.,2.3E+03)); +#5476 = VECTOR('',#5477,1.); +#5477 = DIRECTION('',(1.,0.,-0.)); +#5478 = ORIENTED_EDGE('',*,*,#5479,.T.); +#5479 = EDGE_CURVE('',#5470,#5480,#5482,.T.); +#5480 = VERTEX_POINT('',#5481); +#5481 = CARTESIAN_POINT('',(-390.,660.,2.3E+03)); +#5482 = LINE('',#5483,#5484); +#5483 = CARTESIAN_POINT('',(-390.,602.,2.3E+03)); +#5484 = VECTOR('',#5485,1.); +#5485 = DIRECTION('',(-0.,1.,0.)); +#5486 = ORIENTED_EDGE('',*,*,#5487,.T.); +#5487 = EDGE_CURVE('',#5480,#5488,#5490,.T.); +#5488 = VERTEX_POINT('',#5489); +#5489 = CARTESIAN_POINT('',(390.,660.,2.3E+03)); +#5490 = LINE('',#5491,#5492); +#5491 = CARTESIAN_POINT('',(-390.,660.,2.3E+03)); +#5492 = VECTOR('',#5493,1.); +#5493 = DIRECTION('',(1.,0.,-0.)); +#5494 = ORIENTED_EDGE('',*,*,#5495,.F.); +#5495 = EDGE_CURVE('',#5472,#5488,#5496,.T.); +#5496 = LINE('',#5497,#5498); +#5497 = CARTESIAN_POINT('',(390.,602.,2.3E+03)); +#5498 = VECTOR('',#5499,1.); +#5499 = DIRECTION('',(-0.,1.,0.)); +#5500 = PLANE('',#5501); +#5501 = AXIS2_PLACEMENT_3D('',#5502,#5503,#5504); +#5502 = CARTESIAN_POINT('',(-410.,-700.,2.3E+03)); +#5503 = DIRECTION('',(0.,0.,1.)); +#5504 = DIRECTION('',(1.,0.,-0.)); +#5505 = ADVANCED_FACE('',(#5506),#5512,.T.); +#5506 = FACE_BOUND('',#5507,.T.); +#5507 = EDGE_LOOP('',(#5508,#5509,#5510,#5511)); +#5508 = ORIENTED_EDGE('',*,*,#5362,.F.); +#5509 = ORIENTED_EDGE('',*,*,#5426,.T.); +#5510 = ORIENTED_EDGE('',*,*,#5410,.T.); +#5511 = ORIENTED_EDGE('',*,*,#5378,.F.); +#5512 = PLANE('',#5513); +#5513 = AXIS2_PLACEMENT_3D('',#5514,#5515,#5516); +#5514 = CARTESIAN_POINT('',(410.,-700.,2.3E+03)); +#5515 = DIRECTION('',(1.,0.,-0.)); +#5516 = DIRECTION('',(0.,0.,1.)); +#5517 = ADVANCED_FACE('',(#5518),#5543,.F.); +#5518 = FACE_BOUND('',#5519,.F.); +#5519 = EDGE_LOOP('',(#5520,#5530,#5536,#5537)); +#5520 = ORIENTED_EDGE('',*,*,#5521,.F.); +#5521 = EDGE_CURVE('',#5522,#5524,#5526,.T.); +#5522 = VERTEX_POINT('',#5523); +#5523 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5524 = VERTEX_POINT('',#5525); +#5525 = CARTESIAN_POINT('',(390.,-660.,2.255E+03)); +#5526 = LINE('',#5527,#5528); +#5527 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5528 = VECTOR('',#5529,1.); +#5529 = DIRECTION('',(1.,0.,-0.)); +#5530 = ORIENTED_EDGE('',*,*,#5531,.T.); +#5531 = EDGE_CURVE('',#5522,#5436,#5532,.T.); +#5532 = LINE('',#5533,#5534); +#5533 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5534 = VECTOR('',#5535,1.); +#5535 = DIRECTION('',(0.,0.,1.)); +#5536 = ORIENTED_EDGE('',*,*,#5435,.T.); +#5537 = ORIENTED_EDGE('',*,*,#5538,.F.); +#5538 = EDGE_CURVE('',#5524,#5438,#5539,.T.); +#5539 = LINE('',#5540,#5541); +#5540 = CARTESIAN_POINT('',(390.,-660.,2.255E+03)); +#5541 = VECTOR('',#5542,1.); +#5542 = DIRECTION('',(0.,0.,1.)); +#5543 = PLANE('',#5544); +#5544 = AXIS2_PLACEMENT_3D('',#5545,#5546,#5547); +#5545 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5546 = DIRECTION('',(-0.,1.,0.)); +#5547 = DIRECTION('',(0.,0.,1.)); +#5548 = ADVANCED_FACE('',(#5549),#5567,.F.); +#5549 = FACE_BOUND('',#5550,.F.); +#5550 = EDGE_LOOP('',(#5551,#5552,#5560,#5566)); +#5551 = ORIENTED_EDGE('',*,*,#5531,.F.); +#5552 = ORIENTED_EDGE('',*,*,#5553,.T.); +#5553 = EDGE_CURVE('',#5522,#5554,#5556,.T.); +#5554 = VERTEX_POINT('',#5555); +#5555 = CARTESIAN_POINT('',(-390.,-602.,2.255E+03)); +#5556 = LINE('',#5557,#5558); +#5557 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5558 = VECTOR('',#5559,1.); +#5559 = DIRECTION('',(-0.,1.,0.)); +#5560 = ORIENTED_EDGE('',*,*,#5561,.T.); +#5561 = EDGE_CURVE('',#5554,#5446,#5562,.T.); +#5562 = LINE('',#5563,#5564); +#5563 = CARTESIAN_POINT('',(-390.,-602.,2.255E+03)); +#5564 = VECTOR('',#5565,1.); +#5565 = DIRECTION('',(0.,0.,1.)); +#5566 = ORIENTED_EDGE('',*,*,#5445,.F.); +#5567 = PLANE('',#5568); +#5568 = AXIS2_PLACEMENT_3D('',#5569,#5570,#5571); +#5569 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5570 = DIRECTION('',(1.,0.,-0.)); +#5571 = DIRECTION('',(0.,0.,1.)); +#5572 = ADVANCED_FACE('',(#5573),#5591,.T.); +#5573 = FACE_BOUND('',#5574,.T.); +#5574 = EDGE_LOOP('',(#5575,#5576,#5584,#5590)); +#5575 = ORIENTED_EDGE('',*,*,#5538,.F.); +#5576 = ORIENTED_EDGE('',*,*,#5577,.T.); +#5577 = EDGE_CURVE('',#5524,#5578,#5580,.T.); +#5578 = VERTEX_POINT('',#5579); +#5579 = CARTESIAN_POINT('',(390.,-602.,2.255E+03)); +#5580 = LINE('',#5581,#5582); +#5581 = CARTESIAN_POINT('',(390.,-660.,2.255E+03)); +#5582 = VECTOR('',#5583,1.); +#5583 = DIRECTION('',(-0.,1.,0.)); +#5584 = ORIENTED_EDGE('',*,*,#5585,.T.); +#5585 = EDGE_CURVE('',#5578,#5454,#5586,.T.); +#5586 = LINE('',#5587,#5588); +#5587 = CARTESIAN_POINT('',(390.,-602.,2.255E+03)); +#5588 = VECTOR('',#5589,1.); +#5589 = DIRECTION('',(0.,0.,1.)); +#5590 = ORIENTED_EDGE('',*,*,#5461,.F.); +#5591 = PLANE('',#5592); +#5592 = AXIS2_PLACEMENT_3D('',#5593,#5594,#5595); +#5593 = CARTESIAN_POINT('',(390.,-660.,2.255E+03)); +#5594 = DIRECTION('',(1.,0.,-0.)); +#5595 = DIRECTION('',(0.,0.,1.)); +#5596 = ADVANCED_FACE('',(#5597),#5608,.T.); +#5597 = FACE_BOUND('',#5598,.T.); +#5598 = EDGE_LOOP('',(#5599,#5605,#5606,#5607)); +#5599 = ORIENTED_EDGE('',*,*,#5600,.F.); +#5600 = EDGE_CURVE('',#5554,#5578,#5601,.T.); +#5601 = LINE('',#5602,#5603); +#5602 = CARTESIAN_POINT('',(-390.,-602.,2.255E+03)); +#5603 = VECTOR('',#5604,1.); +#5604 = DIRECTION('',(1.,0.,-0.)); +#5605 = ORIENTED_EDGE('',*,*,#5561,.T.); +#5606 = ORIENTED_EDGE('',*,*,#5453,.T.); +#5607 = ORIENTED_EDGE('',*,*,#5585,.F.); +#5608 = PLANE('',#5609); +#5609 = AXIS2_PLACEMENT_3D('',#5610,#5611,#5612); +#5610 = CARTESIAN_POINT('',(-390.,-602.,2.255E+03)); +#5611 = DIRECTION('',(-0.,1.,0.)); +#5612 = DIRECTION('',(0.,0.,1.)); +#5613 = ADVANCED_FACE('',(#5614),#5639,.F.); +#5614 = FACE_BOUND('',#5615,.F.); +#5615 = EDGE_LOOP('',(#5616,#5626,#5632,#5633)); +#5616 = ORIENTED_EDGE('',*,*,#5617,.F.); +#5617 = EDGE_CURVE('',#5618,#5620,#5622,.T.); +#5618 = VERTEX_POINT('',#5619); +#5619 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5620 = VERTEX_POINT('',#5621); +#5621 = CARTESIAN_POINT('',(390.,602.,2.255E+03)); +#5622 = LINE('',#5623,#5624); +#5623 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5624 = VECTOR('',#5625,1.); +#5625 = DIRECTION('',(1.,0.,-0.)); +#5626 = ORIENTED_EDGE('',*,*,#5627,.T.); +#5627 = EDGE_CURVE('',#5618,#5470,#5628,.T.); +#5628 = LINE('',#5629,#5630); +#5629 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5630 = VECTOR('',#5631,1.); +#5631 = DIRECTION('',(0.,0.,1.)); +#5632 = ORIENTED_EDGE('',*,*,#5469,.T.); +#5633 = ORIENTED_EDGE('',*,*,#5634,.F.); +#5634 = EDGE_CURVE('',#5620,#5472,#5635,.T.); +#5635 = LINE('',#5636,#5637); +#5636 = CARTESIAN_POINT('',(390.,602.,2.255E+03)); +#5637 = VECTOR('',#5638,1.); +#5638 = DIRECTION('',(0.,0.,1.)); +#5639 = PLANE('',#5640); +#5640 = AXIS2_PLACEMENT_3D('',#5641,#5642,#5643); +#5641 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5642 = DIRECTION('',(-0.,1.,0.)); +#5643 = DIRECTION('',(0.,0.,1.)); +#5644 = ADVANCED_FACE('',(#5645),#5663,.T.); +#5645 = FACE_BOUND('',#5646,.T.); +#5646 = EDGE_LOOP('',(#5647,#5648,#5656,#5662)); +#5647 = ORIENTED_EDGE('',*,*,#5634,.F.); +#5648 = ORIENTED_EDGE('',*,*,#5649,.T.); +#5649 = EDGE_CURVE('',#5620,#5650,#5652,.T.); +#5650 = VERTEX_POINT('',#5651); +#5651 = CARTESIAN_POINT('',(390.,660.,2.255E+03)); +#5652 = LINE('',#5653,#5654); +#5653 = CARTESIAN_POINT('',(390.,602.,2.255E+03)); +#5654 = VECTOR('',#5655,1.); +#5655 = DIRECTION('',(-0.,1.,0.)); +#5656 = ORIENTED_EDGE('',*,*,#5657,.T.); +#5657 = EDGE_CURVE('',#5650,#5488,#5658,.T.); +#5658 = LINE('',#5659,#5660); +#5659 = CARTESIAN_POINT('',(390.,660.,2.255E+03)); +#5660 = VECTOR('',#5661,1.); +#5661 = DIRECTION('',(0.,0.,1.)); +#5662 = ORIENTED_EDGE('',*,*,#5495,.F.); +#5663 = PLANE('',#5664); +#5664 = AXIS2_PLACEMENT_3D('',#5665,#5666,#5667); +#5665 = CARTESIAN_POINT('',(390.,602.,2.255E+03)); +#5666 = DIRECTION('',(1.,0.,-0.)); +#5667 = DIRECTION('',(0.,0.,1.)); +#5668 = ADVANCED_FACE('',(#5669),#5687,.T.); +#5669 = FACE_BOUND('',#5670,.T.); +#5670 = EDGE_LOOP('',(#5671,#5679,#5685,#5686)); +#5671 = ORIENTED_EDGE('',*,*,#5672,.F.); +#5672 = EDGE_CURVE('',#5673,#5650,#5675,.T.); +#5673 = VERTEX_POINT('',#5674); +#5674 = CARTESIAN_POINT('',(-390.,660.,2.255E+03)); +#5675 = LINE('',#5676,#5677); +#5676 = CARTESIAN_POINT('',(-390.,660.,2.255E+03)); +#5677 = VECTOR('',#5678,1.); +#5678 = DIRECTION('',(1.,0.,-0.)); +#5679 = ORIENTED_EDGE('',*,*,#5680,.T.); +#5680 = EDGE_CURVE('',#5673,#5480,#5681,.T.); +#5681 = LINE('',#5682,#5683); +#5682 = CARTESIAN_POINT('',(-390.,660.,2.255E+03)); +#5683 = VECTOR('',#5684,1.); +#5684 = DIRECTION('',(0.,0.,1.)); +#5685 = ORIENTED_EDGE('',*,*,#5487,.T.); +#5686 = ORIENTED_EDGE('',*,*,#5657,.F.); +#5687 = PLANE('',#5688); +#5688 = AXIS2_PLACEMENT_3D('',#5689,#5690,#5691); +#5689 = CARTESIAN_POINT('',(-390.,660.,2.255E+03)); +#5690 = DIRECTION('',(-0.,1.,0.)); +#5691 = DIRECTION('',(0.,0.,1.)); +#5692 = ADVANCED_FACE('',(#5693),#5704,.F.); +#5693 = FACE_BOUND('',#5694,.F.); +#5694 = EDGE_LOOP('',(#5695,#5696,#5702,#5703)); +#5695 = ORIENTED_EDGE('',*,*,#5627,.F.); +#5696 = ORIENTED_EDGE('',*,*,#5697,.T.); +#5697 = EDGE_CURVE('',#5618,#5673,#5698,.T.); +#5698 = LINE('',#5699,#5700); +#5699 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5700 = VECTOR('',#5701,1.); +#5701 = DIRECTION('',(-0.,1.,0.)); +#5702 = ORIENTED_EDGE('',*,*,#5680,.T.); +#5703 = ORIENTED_EDGE('',*,*,#5479,.F.); +#5704 = PLANE('',#5705); +#5705 = AXIS2_PLACEMENT_3D('',#5706,#5707,#5708); +#5706 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5707 = DIRECTION('',(1.,0.,-0.)); +#5708 = DIRECTION('',(0.,0.,1.)); +#5709 = ADVANCED_FACE('',(#5710),#5716,.F.); +#5710 = FACE_BOUND('',#5711,.F.); +#5711 = EDGE_LOOP('',(#5712,#5713,#5714,#5715)); +#5712 = ORIENTED_EDGE('',*,*,#5553,.F.); +#5713 = ORIENTED_EDGE('',*,*,#5521,.T.); +#5714 = ORIENTED_EDGE('',*,*,#5577,.T.); +#5715 = ORIENTED_EDGE('',*,*,#5600,.F.); +#5716 = PLANE('',#5717); +#5717 = AXIS2_PLACEMENT_3D('',#5718,#5719,#5720); +#5718 = CARTESIAN_POINT('',(-390.,-660.,2.255E+03)); +#5719 = DIRECTION('',(0.,0.,1.)); +#5720 = DIRECTION('',(1.,0.,-0.)); +#5721 = ADVANCED_FACE('',(#5722),#5728,.F.); +#5722 = FACE_BOUND('',#5723,.F.); +#5723 = EDGE_LOOP('',(#5724,#5725,#5726,#5727)); +#5724 = ORIENTED_EDGE('',*,*,#5697,.F.); +#5725 = ORIENTED_EDGE('',*,*,#5617,.T.); +#5726 = ORIENTED_EDGE('',*,*,#5649,.T.); +#5727 = ORIENTED_EDGE('',*,*,#5672,.F.); +#5728 = PLANE('',#5729); +#5729 = AXIS2_PLACEMENT_3D('',#5730,#5731,#5732); +#5730 = CARTESIAN_POINT('',(-390.,602.,2.255E+03)); +#5731 = DIRECTION('',(0.,0.,1.)); +#5732 = DIRECTION('',(1.,0.,-0.)); +#5733 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5737)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#5734,#5735,#5736)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#5734 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#5735 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#5736 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#5737 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5734, + 'distance_accuracy_value','confusion accuracy'); +#5738 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5739,#5741); +#5739 = ( REPRESENTATION_RELATIONSHIP('','',#5298,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5740) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#5740 = ITEM_DEFINED_TRANSFORMATION('','',#11,#39); +#5741 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #5742); +#5742 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('7','NAU03_Top_Roof','',#5,#5293, + $); +#5743 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5295)); +#5744 = SHAPE_DEFINITION_REPRESENTATION(#5745,#5751); +#5745 = PRODUCT_DEFINITION_SHAPE('','',#5746); +#5746 = PRODUCT_DEFINITION('design','',#5747,#5750); +#5747 = PRODUCT_DEFINITION_FORMATION('','',#5748); +#5748 = PRODUCT('NAU03_Bottom_Base','NAU03_Bottom_Base','',(#5749)); +#5749 = PRODUCT_CONTEXT('',#2,'mechanical'); +#5750 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#5751 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5752),#6262); +#5752 = MANIFOLD_SOLID_BREP('',#5753); +#5753 = CLOSED_SHELL('',(#5754,#5810,#5841,#5865,#5889,#5945,#5974,#6028 + ,#6042,#6075,#6099,#6123,#6140,#6173,#6204,#6221,#6238,#6250)); +#5754 = ADVANCED_FACE('',(#5755),#5805,.F.); +#5755 = FACE_BOUND('',#5756,.F.); +#5756 = EDGE_LOOP('',(#5757,#5767,#5775,#5783,#5791,#5799)); +#5757 = ORIENTED_EDGE('',*,*,#5758,.F.); +#5758 = EDGE_CURVE('',#5759,#5761,#5763,.T.); +#5759 = VERTEX_POINT('',#5760); +#5760 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5761 = VERTEX_POINT('',#5762); +#5762 = CARTESIAN_POINT('',(-392.5,-665.,0.)); +#5763 = LINE('',#5764,#5765); +#5764 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5765 = VECTOR('',#5766,1.); +#5766 = DIRECTION('',(0.,0.,1.)); +#5767 = ORIENTED_EDGE('',*,*,#5768,.T.); +#5768 = EDGE_CURVE('',#5759,#5769,#5771,.T.); +#5769 = VERTEX_POINT('',#5770); +#5770 = CARTESIAN_POINT('',(-392.5,-595.,-110.)); +#5771 = LINE('',#5772,#5773); +#5772 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5773 = VECTOR('',#5774,1.); +#5774 = DIRECTION('',(-0.,1.,0.)); +#5775 = ORIENTED_EDGE('',*,*,#5776,.T.); +#5776 = EDGE_CURVE('',#5769,#5777,#5779,.T.); +#5777 = VERTEX_POINT('',#5778); +#5778 = CARTESIAN_POINT('',(-392.5,595.,-110.)); +#5779 = LINE('',#5780,#5781); +#5780 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5781 = VECTOR('',#5782,1.); +#5782 = DIRECTION('',(-0.,1.,0.)); +#5783 = ORIENTED_EDGE('',*,*,#5784,.T.); +#5784 = EDGE_CURVE('',#5777,#5785,#5787,.T.); +#5785 = VERTEX_POINT('',#5786); +#5786 = CARTESIAN_POINT('',(-392.5,665.,-110.)); +#5787 = LINE('',#5788,#5789); +#5788 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5789 = VECTOR('',#5790,1.); +#5790 = DIRECTION('',(-0.,1.,0.)); +#5791 = ORIENTED_EDGE('',*,*,#5792,.T.); +#5792 = EDGE_CURVE('',#5785,#5793,#5795,.T.); +#5793 = VERTEX_POINT('',#5794); +#5794 = CARTESIAN_POINT('',(-392.5,665.,0.)); +#5795 = LINE('',#5796,#5797); +#5796 = CARTESIAN_POINT('',(-392.5,665.,-110.)); +#5797 = VECTOR('',#5798,1.); +#5798 = DIRECTION('',(0.,0.,1.)); +#5799 = ORIENTED_EDGE('',*,*,#5800,.F.); +#5800 = EDGE_CURVE('',#5761,#5793,#5801,.T.); +#5801 = LINE('',#5802,#5803); +#5802 = CARTESIAN_POINT('',(-392.5,-665.,0.)); +#5803 = VECTOR('',#5804,1.); +#5804 = DIRECTION('',(-0.,1.,0.)); +#5805 = PLANE('',#5806); +#5806 = AXIS2_PLACEMENT_3D('',#5807,#5808,#5809); +#5807 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5808 = DIRECTION('',(1.,0.,-0.)); +#5809 = DIRECTION('',(0.,0.,1.)); +#5810 = ADVANCED_FACE('',(#5811),#5836,.F.); +#5811 = FACE_BOUND('',#5812,.F.); +#5812 = EDGE_LOOP('',(#5813,#5821,#5822,#5830)); +#5813 = ORIENTED_EDGE('',*,*,#5814,.F.); +#5814 = EDGE_CURVE('',#5759,#5815,#5817,.T.); +#5815 = VERTEX_POINT('',#5816); +#5816 = CARTESIAN_POINT('',(392.5,-665.,-110.)); +#5817 = LINE('',#5818,#5819); +#5818 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5819 = VECTOR('',#5820,1.); +#5820 = DIRECTION('',(1.,0.,-0.)); +#5821 = ORIENTED_EDGE('',*,*,#5758,.T.); +#5822 = ORIENTED_EDGE('',*,*,#5823,.T.); +#5823 = EDGE_CURVE('',#5761,#5824,#5826,.T.); +#5824 = VERTEX_POINT('',#5825); +#5825 = CARTESIAN_POINT('',(392.5,-665.,0.)); +#5826 = LINE('',#5827,#5828); +#5827 = CARTESIAN_POINT('',(-392.5,-665.,0.)); +#5828 = VECTOR('',#5829,1.); +#5829 = DIRECTION('',(1.,0.,-0.)); +#5830 = ORIENTED_EDGE('',*,*,#5831,.F.); +#5831 = EDGE_CURVE('',#5815,#5824,#5832,.T.); +#5832 = LINE('',#5833,#5834); +#5833 = CARTESIAN_POINT('',(392.5,-665.,-110.)); +#5834 = VECTOR('',#5835,1.); +#5835 = DIRECTION('',(0.,0.,1.)); +#5836 = PLANE('',#5837); +#5837 = AXIS2_PLACEMENT_3D('',#5838,#5839,#5840); +#5838 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5839 = DIRECTION('',(-0.,1.,0.)); +#5840 = DIRECTION('',(0.,0.,1.)); +#5841 = ADVANCED_FACE('',(#5842),#5860,.T.); +#5842 = FACE_BOUND('',#5843,.T.); +#5843 = EDGE_LOOP('',(#5844,#5845,#5846,#5854)); +#5844 = ORIENTED_EDGE('',*,*,#5800,.F.); +#5845 = ORIENTED_EDGE('',*,*,#5823,.T.); +#5846 = ORIENTED_EDGE('',*,*,#5847,.T.); +#5847 = EDGE_CURVE('',#5824,#5848,#5850,.T.); +#5848 = VERTEX_POINT('',#5849); +#5849 = CARTESIAN_POINT('',(392.5,665.,0.)); +#5850 = LINE('',#5851,#5852); +#5851 = CARTESIAN_POINT('',(392.5,-665.,0.)); +#5852 = VECTOR('',#5853,1.); +#5853 = DIRECTION('',(-0.,1.,0.)); +#5854 = ORIENTED_EDGE('',*,*,#5855,.F.); +#5855 = EDGE_CURVE('',#5793,#5848,#5856,.T.); +#5856 = LINE('',#5857,#5858); +#5857 = CARTESIAN_POINT('',(-392.5,665.,0.)); +#5858 = VECTOR('',#5859,1.); +#5859 = DIRECTION('',(1.,0.,-0.)); +#5860 = PLANE('',#5861); +#5861 = AXIS2_PLACEMENT_3D('',#5862,#5863,#5864); +#5862 = CARTESIAN_POINT('',(-392.5,-665.,0.)); +#5863 = DIRECTION('',(0.,0.,1.)); +#5864 = DIRECTION('',(1.,0.,-0.)); +#5865 = ADVANCED_FACE('',(#5866),#5884,.T.); +#5866 = FACE_BOUND('',#5867,.T.); +#5867 = EDGE_LOOP('',(#5868,#5876,#5877,#5878)); +#5868 = ORIENTED_EDGE('',*,*,#5869,.F.); +#5869 = EDGE_CURVE('',#5785,#5870,#5872,.T.); +#5870 = VERTEX_POINT('',#5871); +#5871 = CARTESIAN_POINT('',(392.5,665.,-110.)); +#5872 = LINE('',#5873,#5874); +#5873 = CARTESIAN_POINT('',(-392.5,665.,-110.)); +#5874 = VECTOR('',#5875,1.); +#5875 = DIRECTION('',(1.,0.,-0.)); +#5876 = ORIENTED_EDGE('',*,*,#5792,.T.); +#5877 = ORIENTED_EDGE('',*,*,#5855,.T.); +#5878 = ORIENTED_EDGE('',*,*,#5879,.F.); +#5879 = EDGE_CURVE('',#5870,#5848,#5880,.T.); +#5880 = LINE('',#5881,#5882); +#5881 = CARTESIAN_POINT('',(392.5,665.,-110.)); +#5882 = VECTOR('',#5883,1.); +#5883 = DIRECTION('',(0.,0.,1.)); +#5884 = PLANE('',#5885); +#5885 = AXIS2_PLACEMENT_3D('',#5886,#5887,#5888); +#5886 = CARTESIAN_POINT('',(-392.5,665.,-110.)); +#5887 = DIRECTION('',(-0.,1.,0.)); +#5888 = DIRECTION('',(0.,0.,1.)); +#5889 = ADVANCED_FACE('',(#5890),#5940,.T.); +#5890 = FACE_BOUND('',#5891,.T.); +#5891 = EDGE_LOOP('',(#5892,#5900,#5908,#5916,#5924,#5932,#5938,#5939)); +#5892 = ORIENTED_EDGE('',*,*,#5893,.F.); +#5893 = EDGE_CURVE('',#5894,#5769,#5896,.T.); +#5894 = VERTEX_POINT('',#5895); +#5895 = CARTESIAN_POINT('',(-417.5,-595.,-110.)); +#5896 = LINE('',#5897,#5898); +#5897 = CARTESIAN_POINT('',(-417.5,-595.,-110.)); +#5898 = VECTOR('',#5899,1.); +#5899 = DIRECTION('',(1.,0.,-0.)); +#5900 = ORIENTED_EDGE('',*,*,#5901,.F.); +#5901 = EDGE_CURVE('',#5902,#5894,#5904,.T.); +#5902 = VERTEX_POINT('',#5903); +#5903 = CARTESIAN_POINT('',(-417.5,-690.,-110.)); +#5904 = LINE('',#5905,#5906); +#5905 = CARTESIAN_POINT('',(-417.5,-690.,-110.)); +#5906 = VECTOR('',#5907,1.); +#5907 = DIRECTION('',(-0.,1.,0.)); +#5908 = ORIENTED_EDGE('',*,*,#5909,.T.); +#5909 = EDGE_CURVE('',#5902,#5910,#5912,.T.); +#5910 = VERTEX_POINT('',#5911); +#5911 = CARTESIAN_POINT('',(417.5,-690.,-110.)); +#5912 = LINE('',#5913,#5914); +#5913 = CARTESIAN_POINT('',(-417.5,-690.,-110.)); +#5914 = VECTOR('',#5915,1.); +#5915 = DIRECTION('',(1.,0.,-0.)); +#5916 = ORIENTED_EDGE('',*,*,#5917,.T.); +#5917 = EDGE_CURVE('',#5910,#5918,#5920,.T.); +#5918 = VERTEX_POINT('',#5919); +#5919 = CARTESIAN_POINT('',(417.5,-595.,-110.)); +#5920 = LINE('',#5921,#5922); +#5921 = CARTESIAN_POINT('',(417.5,-690.,-110.)); +#5922 = VECTOR('',#5923,1.); +#5923 = DIRECTION('',(-0.,1.,0.)); +#5924 = ORIENTED_EDGE('',*,*,#5925,.F.); +#5925 = EDGE_CURVE('',#5926,#5918,#5928,.T.); +#5926 = VERTEX_POINT('',#5927); +#5927 = CARTESIAN_POINT('',(392.5,-595.,-110.)); +#5928 = LINE('',#5929,#5930); +#5929 = CARTESIAN_POINT('',(-417.5,-595.,-110.)); +#5930 = VECTOR('',#5931,1.); +#5931 = DIRECTION('',(1.,0.,-0.)); +#5932 = ORIENTED_EDGE('',*,*,#5933,.F.); +#5933 = EDGE_CURVE('',#5815,#5926,#5934,.T.); +#5934 = LINE('',#5935,#5936); +#5935 = CARTESIAN_POINT('',(392.5,-665.,-110.)); +#5936 = VECTOR('',#5937,1.); +#5937 = DIRECTION('',(-0.,1.,0.)); +#5938 = ORIENTED_EDGE('',*,*,#5814,.F.); +#5939 = ORIENTED_EDGE('',*,*,#5768,.T.); +#5940 = PLANE('',#5941); +#5941 = AXIS2_PLACEMENT_3D('',#5942,#5943,#5944); +#5942 = CARTESIAN_POINT('',(-417.5,-690.,-110.)); +#5943 = DIRECTION('',(0.,0.,1.)); +#5944 = DIRECTION('',(1.,0.,-0.)); +#5945 = ADVANCED_FACE('',(#5946),#5969,.F.); +#5946 = FACE_BOUND('',#5947,.F.); +#5947 = EDGE_LOOP('',(#5948,#5949,#5955,#5963)); +#5948 = ORIENTED_EDGE('',*,*,#5776,.F.); +#5949 = ORIENTED_EDGE('',*,*,#5950,.T.); +#5950 = EDGE_CURVE('',#5769,#5926,#5951,.T.); +#5951 = LINE('',#5952,#5953); +#5952 = CARTESIAN_POINT('',(-417.5,-595.,-110.)); +#5953 = VECTOR('',#5954,1.); +#5954 = DIRECTION('',(1.,0.,-0.)); +#5955 = ORIENTED_EDGE('',*,*,#5956,.T.); +#5956 = EDGE_CURVE('',#5926,#5957,#5959,.T.); +#5957 = VERTEX_POINT('',#5958); +#5958 = CARTESIAN_POINT('',(392.5,595.,-110.)); +#5959 = LINE('',#5960,#5961); +#5960 = CARTESIAN_POINT('',(392.5,-665.,-110.)); +#5961 = VECTOR('',#5962,1.); +#5962 = DIRECTION('',(-0.,1.,0.)); +#5963 = ORIENTED_EDGE('',*,*,#5964,.F.); +#5964 = EDGE_CURVE('',#5777,#5957,#5965,.T.); +#5965 = LINE('',#5966,#5967); +#5966 = CARTESIAN_POINT('',(-417.5,595.,-110.)); +#5967 = VECTOR('',#5968,1.); +#5968 = DIRECTION('',(1.,0.,-0.)); +#5969 = PLANE('',#5970); +#5970 = AXIS2_PLACEMENT_3D('',#5971,#5972,#5973); +#5971 = CARTESIAN_POINT('',(-392.5,-665.,-110.)); +#5972 = DIRECTION('',(0.,0.,1.)); +#5973 = DIRECTION('',(1.,0.,-0.)); +#5974 = ADVANCED_FACE('',(#5975),#6023,.T.); +#5975 = FACE_BOUND('',#5976,.T.); +#5976 = EDGE_LOOP('',(#5977,#5987,#5995,#6001,#6002,#6003,#6009,#6017)); +#5977 = ORIENTED_EDGE('',*,*,#5978,.F.); +#5978 = EDGE_CURVE('',#5979,#5981,#5983,.T.); +#5979 = VERTEX_POINT('',#5980); +#5980 = CARTESIAN_POINT('',(-417.5,690.,-110.)); +#5981 = VERTEX_POINT('',#5982); +#5982 = CARTESIAN_POINT('',(417.5,690.,-110.)); +#5983 = LINE('',#5984,#5985); +#5984 = CARTESIAN_POINT('',(-417.5,690.,-110.)); +#5985 = VECTOR('',#5986,1.); +#5986 = DIRECTION('',(1.,0.,-0.)); +#5987 = ORIENTED_EDGE('',*,*,#5988,.F.); +#5988 = EDGE_CURVE('',#5989,#5979,#5991,.T.); +#5989 = VERTEX_POINT('',#5990); +#5990 = CARTESIAN_POINT('',(-417.5,595.,-110.)); +#5991 = LINE('',#5992,#5993); +#5992 = CARTESIAN_POINT('',(-417.5,595.,-110.)); +#5993 = VECTOR('',#5994,1.); +#5994 = DIRECTION('',(-0.,1.,0.)); +#5995 = ORIENTED_EDGE('',*,*,#5996,.T.); +#5996 = EDGE_CURVE('',#5989,#5777,#5997,.T.); +#5997 = LINE('',#5998,#5999); +#5998 = CARTESIAN_POINT('',(-417.5,595.,-110.)); +#5999 = VECTOR('',#6000,1.); +#6000 = DIRECTION('',(1.,0.,-0.)); +#6001 = ORIENTED_EDGE('',*,*,#5784,.T.); +#6002 = ORIENTED_EDGE('',*,*,#5869,.T.); +#6003 = ORIENTED_EDGE('',*,*,#6004,.F.); +#6004 = EDGE_CURVE('',#5957,#5870,#6005,.T.); +#6005 = LINE('',#6006,#6007); +#6006 = CARTESIAN_POINT('',(392.5,-665.,-110.)); +#6007 = VECTOR('',#6008,1.); +#6008 = DIRECTION('',(-0.,1.,0.)); +#6009 = ORIENTED_EDGE('',*,*,#6010,.T.); +#6010 = EDGE_CURVE('',#5957,#6011,#6013,.T.); +#6011 = VERTEX_POINT('',#6012); +#6012 = CARTESIAN_POINT('',(417.5,595.,-110.)); +#6013 = LINE('',#6014,#6015); +#6014 = CARTESIAN_POINT('',(-417.5,595.,-110.)); +#6015 = VECTOR('',#6016,1.); +#6016 = DIRECTION('',(1.,0.,-0.)); +#6017 = ORIENTED_EDGE('',*,*,#6018,.T.); +#6018 = EDGE_CURVE('',#6011,#5981,#6019,.T.); +#6019 = LINE('',#6020,#6021); +#6020 = CARTESIAN_POINT('',(417.5,595.,-110.)); +#6021 = VECTOR('',#6022,1.); +#6022 = DIRECTION('',(-0.,1.,0.)); +#6023 = PLANE('',#6024); +#6024 = AXIS2_PLACEMENT_3D('',#6025,#6026,#6027); +#6025 = CARTESIAN_POINT('',(-417.5,595.,-110.)); +#6026 = DIRECTION('',(0.,0.,1.)); +#6027 = DIRECTION('',(1.,0.,-0.)); +#6028 = ADVANCED_FACE('',(#6029),#6037,.T.); +#6029 = FACE_BOUND('',#6030,.T.); +#6030 = EDGE_LOOP('',(#6031,#6032,#6033,#6034,#6035,#6036)); +#6031 = ORIENTED_EDGE('',*,*,#5831,.F.); +#6032 = ORIENTED_EDGE('',*,*,#5933,.T.); +#6033 = ORIENTED_EDGE('',*,*,#5956,.T.); +#6034 = ORIENTED_EDGE('',*,*,#6004,.T.); +#6035 = ORIENTED_EDGE('',*,*,#5879,.T.); +#6036 = ORIENTED_EDGE('',*,*,#5847,.F.); +#6037 = PLANE('',#6038); +#6038 = AXIS2_PLACEMENT_3D('',#6039,#6040,#6041); +#6039 = CARTESIAN_POINT('',(392.5,-665.,-110.)); +#6040 = DIRECTION('',(1.,0.,-0.)); +#6041 = DIRECTION('',(0.,0.,1.)); +#6042 = ADVANCED_FACE('',(#6043),#6070,.T.); +#6043 = FACE_BOUND('',#6044,.T.); +#6044 = EDGE_LOOP('',(#6045,#6055,#6061,#6062,#6063,#6064)); +#6045 = ORIENTED_EDGE('',*,*,#6046,.F.); +#6046 = EDGE_CURVE('',#6047,#6049,#6051,.T.); +#6047 = VERTEX_POINT('',#6048); +#6048 = CARTESIAN_POINT('',(-417.5,-595.,-168.)); +#6049 = VERTEX_POINT('',#6050); +#6050 = CARTESIAN_POINT('',(417.5,-595.,-168.)); +#6051 = LINE('',#6052,#6053); +#6052 = CARTESIAN_POINT('',(-417.5,-595.,-168.)); +#6053 = VECTOR('',#6054,1.); +#6054 = DIRECTION('',(1.,0.,-0.)); +#6055 = ORIENTED_EDGE('',*,*,#6056,.T.); +#6056 = EDGE_CURVE('',#6047,#5894,#6057,.T.); +#6057 = LINE('',#6058,#6059); +#6058 = CARTESIAN_POINT('',(-417.5,-595.,-168.)); +#6059 = VECTOR('',#6060,1.); +#6060 = DIRECTION('',(0.,0.,1.)); +#6061 = ORIENTED_EDGE('',*,*,#5893,.T.); +#6062 = ORIENTED_EDGE('',*,*,#5950,.T.); +#6063 = ORIENTED_EDGE('',*,*,#5925,.T.); +#6064 = ORIENTED_EDGE('',*,*,#6065,.F.); +#6065 = EDGE_CURVE('',#6049,#5918,#6066,.T.); +#6066 = LINE('',#6067,#6068); +#6067 = CARTESIAN_POINT('',(417.5,-595.,-168.)); +#6068 = VECTOR('',#6069,1.); +#6069 = DIRECTION('',(0.,0.,1.)); +#6070 = PLANE('',#6071); +#6071 = AXIS2_PLACEMENT_3D('',#6072,#6073,#6074); +#6072 = CARTESIAN_POINT('',(-417.5,-595.,-168.)); +#6073 = DIRECTION('',(-0.,1.,0.)); +#6074 = DIRECTION('',(0.,0.,1.)); +#6075 = ADVANCED_FACE('',(#6076),#6094,.T.); +#6076 = FACE_BOUND('',#6077,.T.); +#6077 = EDGE_LOOP('',(#6078,#6086,#6092,#6093)); +#6078 = ORIENTED_EDGE('',*,*,#6079,.F.); +#6079 = EDGE_CURVE('',#6080,#5910,#6082,.T.); +#6080 = VERTEX_POINT('',#6081); +#6081 = CARTESIAN_POINT('',(417.5,-690.,-168.)); +#6082 = LINE('',#6083,#6084); +#6083 = CARTESIAN_POINT('',(417.5,-690.,-168.)); +#6084 = VECTOR('',#6085,1.); +#6085 = DIRECTION('',(0.,0.,1.)); +#6086 = ORIENTED_EDGE('',*,*,#6087,.T.); +#6087 = EDGE_CURVE('',#6080,#6049,#6088,.T.); +#6088 = LINE('',#6089,#6090); +#6089 = CARTESIAN_POINT('',(417.5,-690.,-168.)); +#6090 = VECTOR('',#6091,1.); +#6091 = DIRECTION('',(-0.,1.,0.)); +#6092 = ORIENTED_EDGE('',*,*,#6065,.T.); +#6093 = ORIENTED_EDGE('',*,*,#5917,.F.); +#6094 = PLANE('',#6095); +#6095 = AXIS2_PLACEMENT_3D('',#6096,#6097,#6098); +#6096 = CARTESIAN_POINT('',(417.5,-690.,-168.)); +#6097 = DIRECTION('',(1.,0.,-0.)); +#6098 = DIRECTION('',(0.,0.,1.)); +#6099 = ADVANCED_FACE('',(#6100),#6118,.F.); +#6100 = FACE_BOUND('',#6101,.F.); +#6101 = EDGE_LOOP('',(#6102,#6110,#6116,#6117)); +#6102 = ORIENTED_EDGE('',*,*,#6103,.F.); +#6103 = EDGE_CURVE('',#6104,#6080,#6106,.T.); +#6104 = VERTEX_POINT('',#6105); +#6105 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6106 = LINE('',#6107,#6108); +#6107 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6108 = VECTOR('',#6109,1.); +#6109 = DIRECTION('',(1.,0.,-0.)); +#6110 = ORIENTED_EDGE('',*,*,#6111,.T.); +#6111 = EDGE_CURVE('',#6104,#5902,#6112,.T.); +#6112 = LINE('',#6113,#6114); +#6113 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6114 = VECTOR('',#6115,1.); +#6115 = DIRECTION('',(0.,0.,1.)); +#6116 = ORIENTED_EDGE('',*,*,#5909,.T.); +#6117 = ORIENTED_EDGE('',*,*,#6079,.F.); +#6118 = PLANE('',#6119); +#6119 = AXIS2_PLACEMENT_3D('',#6120,#6121,#6122); +#6120 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6121 = DIRECTION('',(-0.,1.,0.)); +#6122 = DIRECTION('',(0.,0.,1.)); +#6123 = ADVANCED_FACE('',(#6124),#6135,.F.); +#6124 = FACE_BOUND('',#6125,.F.); +#6125 = EDGE_LOOP('',(#6126,#6127,#6133,#6134)); +#6126 = ORIENTED_EDGE('',*,*,#6111,.F.); +#6127 = ORIENTED_EDGE('',*,*,#6128,.T.); +#6128 = EDGE_CURVE('',#6104,#6047,#6129,.T.); +#6129 = LINE('',#6130,#6131); +#6130 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6131 = VECTOR('',#6132,1.); +#6132 = DIRECTION('',(-0.,1.,0.)); +#6133 = ORIENTED_EDGE('',*,*,#6056,.T.); +#6134 = ORIENTED_EDGE('',*,*,#5901,.F.); +#6135 = PLANE('',#6136); +#6136 = AXIS2_PLACEMENT_3D('',#6137,#6138,#6139); +#6137 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6138 = DIRECTION('',(1.,0.,-0.)); +#6139 = DIRECTION('',(0.,0.,1.)); +#6140 = ADVANCED_FACE('',(#6141),#6168,.F.); +#6141 = FACE_BOUND('',#6142,.F.); +#6142 = EDGE_LOOP('',(#6143,#6153,#6159,#6160,#6161,#6162)); +#6143 = ORIENTED_EDGE('',*,*,#6144,.F.); +#6144 = EDGE_CURVE('',#6145,#6147,#6149,.T.); +#6145 = VERTEX_POINT('',#6146); +#6146 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6147 = VERTEX_POINT('',#6148); +#6148 = CARTESIAN_POINT('',(417.5,595.,-168.)); +#6149 = LINE('',#6150,#6151); +#6150 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6151 = VECTOR('',#6152,1.); +#6152 = DIRECTION('',(1.,0.,-0.)); +#6153 = ORIENTED_EDGE('',*,*,#6154,.T.); +#6154 = EDGE_CURVE('',#6145,#5989,#6155,.T.); +#6155 = LINE('',#6156,#6157); +#6156 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6157 = VECTOR('',#6158,1.); +#6158 = DIRECTION('',(0.,0.,1.)); +#6159 = ORIENTED_EDGE('',*,*,#5996,.T.); +#6160 = ORIENTED_EDGE('',*,*,#5964,.T.); +#6161 = ORIENTED_EDGE('',*,*,#6010,.T.); +#6162 = ORIENTED_EDGE('',*,*,#6163,.F.); +#6163 = EDGE_CURVE('',#6147,#6011,#6164,.T.); +#6164 = LINE('',#6165,#6166); +#6165 = CARTESIAN_POINT('',(417.5,595.,-168.)); +#6166 = VECTOR('',#6167,1.); +#6167 = DIRECTION('',(0.,0.,1.)); +#6168 = PLANE('',#6169); +#6169 = AXIS2_PLACEMENT_3D('',#6170,#6171,#6172); +#6170 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6171 = DIRECTION('',(-0.,1.,0.)); +#6172 = DIRECTION('',(0.,0.,1.)); +#6173 = ADVANCED_FACE('',(#6174),#6199,.T.); +#6174 = FACE_BOUND('',#6175,.T.); +#6175 = EDGE_LOOP('',(#6176,#6186,#6192,#6193)); +#6176 = ORIENTED_EDGE('',*,*,#6177,.F.); +#6177 = EDGE_CURVE('',#6178,#6180,#6182,.T.); +#6178 = VERTEX_POINT('',#6179); +#6179 = CARTESIAN_POINT('',(-417.5,690.,-168.)); +#6180 = VERTEX_POINT('',#6181); +#6181 = CARTESIAN_POINT('',(417.5,690.,-168.)); +#6182 = LINE('',#6183,#6184); +#6183 = CARTESIAN_POINT('',(-417.5,690.,-168.)); +#6184 = VECTOR('',#6185,1.); +#6185 = DIRECTION('',(1.,0.,-0.)); +#6186 = ORIENTED_EDGE('',*,*,#6187,.T.); +#6187 = EDGE_CURVE('',#6178,#5979,#6188,.T.); +#6188 = LINE('',#6189,#6190); +#6189 = CARTESIAN_POINT('',(-417.5,690.,-168.)); +#6190 = VECTOR('',#6191,1.); +#6191 = DIRECTION('',(0.,0.,1.)); +#6192 = ORIENTED_EDGE('',*,*,#5978,.T.); +#6193 = ORIENTED_EDGE('',*,*,#6194,.F.); +#6194 = EDGE_CURVE('',#6180,#5981,#6195,.T.); +#6195 = LINE('',#6196,#6197); +#6196 = CARTESIAN_POINT('',(417.5,690.,-168.)); +#6197 = VECTOR('',#6198,1.); +#6198 = DIRECTION('',(0.,0.,1.)); +#6199 = PLANE('',#6200); +#6200 = AXIS2_PLACEMENT_3D('',#6201,#6202,#6203); +#6201 = CARTESIAN_POINT('',(-417.5,690.,-168.)); +#6202 = DIRECTION('',(-0.,1.,0.)); +#6203 = DIRECTION('',(0.,0.,1.)); +#6204 = ADVANCED_FACE('',(#6205),#6216,.T.); +#6205 = FACE_BOUND('',#6206,.T.); +#6206 = EDGE_LOOP('',(#6207,#6208,#6214,#6215)); +#6207 = ORIENTED_EDGE('',*,*,#6163,.F.); +#6208 = ORIENTED_EDGE('',*,*,#6209,.T.); +#6209 = EDGE_CURVE('',#6147,#6180,#6210,.T.); +#6210 = LINE('',#6211,#6212); +#6211 = CARTESIAN_POINT('',(417.5,595.,-168.)); +#6212 = VECTOR('',#6213,1.); +#6213 = DIRECTION('',(-0.,1.,0.)); +#6214 = ORIENTED_EDGE('',*,*,#6194,.T.); +#6215 = ORIENTED_EDGE('',*,*,#6018,.F.); +#6216 = PLANE('',#6217); +#6217 = AXIS2_PLACEMENT_3D('',#6218,#6219,#6220); +#6218 = CARTESIAN_POINT('',(417.5,595.,-168.)); +#6219 = DIRECTION('',(1.,0.,-0.)); +#6220 = DIRECTION('',(0.,0.,1.)); +#6221 = ADVANCED_FACE('',(#6222),#6233,.F.); +#6222 = FACE_BOUND('',#6223,.F.); +#6223 = EDGE_LOOP('',(#6224,#6225,#6231,#6232)); +#6224 = ORIENTED_EDGE('',*,*,#6154,.F.); +#6225 = ORIENTED_EDGE('',*,*,#6226,.T.); +#6226 = EDGE_CURVE('',#6145,#6178,#6227,.T.); +#6227 = LINE('',#6228,#6229); +#6228 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6229 = VECTOR('',#6230,1.); +#6230 = DIRECTION('',(-0.,1.,0.)); +#6231 = ORIENTED_EDGE('',*,*,#6187,.T.); +#6232 = ORIENTED_EDGE('',*,*,#5988,.F.); +#6233 = PLANE('',#6234); +#6234 = AXIS2_PLACEMENT_3D('',#6235,#6236,#6237); +#6235 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6236 = DIRECTION('',(1.,0.,-0.)); +#6237 = DIRECTION('',(0.,0.,1.)); +#6238 = ADVANCED_FACE('',(#6239),#6245,.F.); +#6239 = FACE_BOUND('',#6240,.F.); +#6240 = EDGE_LOOP('',(#6241,#6242,#6243,#6244)); +#6241 = ORIENTED_EDGE('',*,*,#6128,.F.); +#6242 = ORIENTED_EDGE('',*,*,#6103,.T.); +#6243 = ORIENTED_EDGE('',*,*,#6087,.T.); +#6244 = ORIENTED_EDGE('',*,*,#6046,.F.); +#6245 = PLANE('',#6246); +#6246 = AXIS2_PLACEMENT_3D('',#6247,#6248,#6249); +#6247 = CARTESIAN_POINT('',(-417.5,-690.,-168.)); +#6248 = DIRECTION('',(0.,0.,1.)); +#6249 = DIRECTION('',(1.,0.,-0.)); +#6250 = ADVANCED_FACE('',(#6251),#6257,.F.); +#6251 = FACE_BOUND('',#6252,.F.); +#6252 = EDGE_LOOP('',(#6253,#6254,#6255,#6256)); +#6253 = ORIENTED_EDGE('',*,*,#6226,.F.); +#6254 = ORIENTED_EDGE('',*,*,#6144,.T.); +#6255 = ORIENTED_EDGE('',*,*,#6209,.T.); +#6256 = ORIENTED_EDGE('',*,*,#6177,.F.); +#6257 = PLANE('',#6258); +#6258 = AXIS2_PLACEMENT_3D('',#6259,#6260,#6261); +#6259 = CARTESIAN_POINT('',(-417.5,595.,-168.)); +#6260 = DIRECTION('',(0.,0.,1.)); +#6261 = DIRECTION('',(1.,0.,-0.)); +#6262 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6266)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#6263,#6264,#6265)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#6263 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6264 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#6265 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#6266 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6263, + 'distance_accuracy_value','confusion accuracy'); +#6267 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6268,#6270); +#6268 = ( REPRESENTATION_RELATIONSHIP('','',#5751,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6269) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#6269 = ITEM_DEFINED_TRANSFORMATION('','',#11,#43); +#6270 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #6271); +#6271 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('8','NAU03_Bottom_Base','',#5, + #5746,$); +#6272 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5748)); +#6273 = SHAPE_DEFINITION_REPRESENTATION(#6274,#6280); +#6274 = PRODUCT_DEFINITION_SHAPE('','',#6275); +#6275 = PRODUCT_DEFINITION('design','',#6276,#6279); +#6276 = PRODUCT_DEFINITION_FORMATION('','',#6277); +#6277 = PRODUCT('NAU03_Interior_Mounting_Plate', + 'NAU03_Interior_Mounting_Plate','',(#6278)); +#6278 = PRODUCT_CONTEXT('',#2,'mechanical'); +#6279 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#6280 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6281),#6431); +#6281 = MANIFOLD_SOLID_BREP('',#6282); +#6282 = CLOSED_SHELL('',(#6283,#6323,#6363,#6385,#6407,#6419)); +#6283 = ADVANCED_FACE('',(#6284),#6318,.F.); +#6284 = FACE_BOUND('',#6285,.F.); +#6285 = EDGE_LOOP('',(#6286,#6296,#6304,#6312)); +#6286 = ORIENTED_EDGE('',*,*,#6287,.F.); +#6287 = EDGE_CURVE('',#6288,#6290,#6292,.T.); +#6288 = VERTEX_POINT('',#6289); +#6289 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6290 = VERTEX_POINT('',#6291); +#6291 = CARTESIAN_POINT('',(-292.5,568.,2.12E+03)); +#6292 = LINE('',#6293,#6294); +#6293 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6294 = VECTOR('',#6295,1.); +#6295 = DIRECTION('',(0.,0.,1.)); +#6296 = ORIENTED_EDGE('',*,*,#6297,.T.); +#6297 = EDGE_CURVE('',#6288,#6298,#6300,.T.); +#6298 = VERTEX_POINT('',#6299); +#6299 = CARTESIAN_POINT('',(-292.5,582.,180.)); +#6300 = LINE('',#6301,#6302); +#6301 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6302 = VECTOR('',#6303,1.); +#6303 = DIRECTION('',(-0.,1.,0.)); +#6304 = ORIENTED_EDGE('',*,*,#6305,.T.); +#6305 = EDGE_CURVE('',#6298,#6306,#6308,.T.); +#6306 = VERTEX_POINT('',#6307); +#6307 = CARTESIAN_POINT('',(-292.5,582.,2.12E+03)); +#6308 = LINE('',#6309,#6310); +#6309 = CARTESIAN_POINT('',(-292.5,582.,180.)); +#6310 = VECTOR('',#6311,1.); +#6311 = DIRECTION('',(0.,0.,1.)); +#6312 = ORIENTED_EDGE('',*,*,#6313,.F.); +#6313 = EDGE_CURVE('',#6290,#6306,#6314,.T.); +#6314 = LINE('',#6315,#6316); +#6315 = CARTESIAN_POINT('',(-292.5,568.,2.12E+03)); +#6316 = VECTOR('',#6317,1.); +#6317 = DIRECTION('',(-0.,1.,0.)); +#6318 = PLANE('',#6319); +#6319 = AXIS2_PLACEMENT_3D('',#6320,#6321,#6322); +#6320 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6321 = DIRECTION('',(1.,0.,-0.)); +#6322 = DIRECTION('',(0.,0.,1.)); +#6323 = ADVANCED_FACE('',(#6324),#6358,.T.); +#6324 = FACE_BOUND('',#6325,.T.); +#6325 = EDGE_LOOP('',(#6326,#6336,#6344,#6352)); +#6326 = ORIENTED_EDGE('',*,*,#6327,.F.); +#6327 = EDGE_CURVE('',#6328,#6330,#6332,.T.); +#6328 = VERTEX_POINT('',#6329); +#6329 = CARTESIAN_POINT('',(292.5,568.,180.)); +#6330 = VERTEX_POINT('',#6331); +#6331 = CARTESIAN_POINT('',(292.5,568.,2.12E+03)); +#6332 = LINE('',#6333,#6334); +#6333 = CARTESIAN_POINT('',(292.5,568.,180.)); +#6334 = VECTOR('',#6335,1.); +#6335 = DIRECTION('',(0.,0.,1.)); +#6336 = ORIENTED_EDGE('',*,*,#6337,.T.); +#6337 = EDGE_CURVE('',#6328,#6338,#6340,.T.); +#6338 = VERTEX_POINT('',#6339); +#6339 = CARTESIAN_POINT('',(292.5,582.,180.)); +#6340 = LINE('',#6341,#6342); +#6341 = CARTESIAN_POINT('',(292.5,568.,180.)); +#6342 = VECTOR('',#6343,1.); +#6343 = DIRECTION('',(-0.,1.,0.)); +#6344 = ORIENTED_EDGE('',*,*,#6345,.T.); +#6345 = EDGE_CURVE('',#6338,#6346,#6348,.T.); +#6346 = VERTEX_POINT('',#6347); +#6347 = CARTESIAN_POINT('',(292.5,582.,2.12E+03)); +#6348 = LINE('',#6349,#6350); +#6349 = CARTESIAN_POINT('',(292.5,582.,180.)); +#6350 = VECTOR('',#6351,1.); +#6351 = DIRECTION('',(0.,0.,1.)); +#6352 = ORIENTED_EDGE('',*,*,#6353,.F.); +#6353 = EDGE_CURVE('',#6330,#6346,#6354,.T.); +#6354 = LINE('',#6355,#6356); +#6355 = CARTESIAN_POINT('',(292.5,568.,2.12E+03)); +#6356 = VECTOR('',#6357,1.); +#6357 = DIRECTION('',(-0.,1.,0.)); +#6358 = PLANE('',#6359); +#6359 = AXIS2_PLACEMENT_3D('',#6360,#6361,#6362); +#6360 = CARTESIAN_POINT('',(292.5,568.,180.)); +#6361 = DIRECTION('',(1.,0.,-0.)); +#6362 = DIRECTION('',(0.,0.,1.)); +#6363 = ADVANCED_FACE('',(#6364),#6380,.F.); +#6364 = FACE_BOUND('',#6365,.F.); +#6365 = EDGE_LOOP('',(#6366,#6372,#6373,#6379)); +#6366 = ORIENTED_EDGE('',*,*,#6367,.F.); +#6367 = EDGE_CURVE('',#6288,#6328,#6368,.T.); +#6368 = LINE('',#6369,#6370); +#6369 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6370 = VECTOR('',#6371,1.); +#6371 = DIRECTION('',(1.,0.,-0.)); +#6372 = ORIENTED_EDGE('',*,*,#6287,.T.); +#6373 = ORIENTED_EDGE('',*,*,#6374,.T.); +#6374 = EDGE_CURVE('',#6290,#6330,#6375,.T.); +#6375 = LINE('',#6376,#6377); +#6376 = CARTESIAN_POINT('',(-292.5,568.,2.12E+03)); +#6377 = VECTOR('',#6378,1.); +#6378 = DIRECTION('',(1.,0.,-0.)); +#6379 = ORIENTED_EDGE('',*,*,#6327,.F.); +#6380 = PLANE('',#6381); +#6381 = AXIS2_PLACEMENT_3D('',#6382,#6383,#6384); +#6382 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6383 = DIRECTION('',(-0.,1.,0.)); +#6384 = DIRECTION('',(0.,0.,1.)); +#6385 = ADVANCED_FACE('',(#6386),#6402,.T.); +#6386 = FACE_BOUND('',#6387,.T.); +#6387 = EDGE_LOOP('',(#6388,#6394,#6395,#6401)); +#6388 = ORIENTED_EDGE('',*,*,#6389,.F.); +#6389 = EDGE_CURVE('',#6298,#6338,#6390,.T.); +#6390 = LINE('',#6391,#6392); +#6391 = CARTESIAN_POINT('',(-292.5,582.,180.)); +#6392 = VECTOR('',#6393,1.); +#6393 = DIRECTION('',(1.,0.,-0.)); +#6394 = ORIENTED_EDGE('',*,*,#6305,.T.); +#6395 = ORIENTED_EDGE('',*,*,#6396,.T.); +#6396 = EDGE_CURVE('',#6306,#6346,#6397,.T.); +#6397 = LINE('',#6398,#6399); +#6398 = CARTESIAN_POINT('',(-292.5,582.,2.12E+03)); +#6399 = VECTOR('',#6400,1.); +#6400 = DIRECTION('',(1.,0.,-0.)); +#6401 = ORIENTED_EDGE('',*,*,#6345,.F.); +#6402 = PLANE('',#6403); +#6403 = AXIS2_PLACEMENT_3D('',#6404,#6405,#6406); +#6404 = CARTESIAN_POINT('',(-292.5,582.,180.)); +#6405 = DIRECTION('',(-0.,1.,0.)); +#6406 = DIRECTION('',(0.,0.,1.)); +#6407 = ADVANCED_FACE('',(#6408),#6414,.F.); +#6408 = FACE_BOUND('',#6409,.F.); +#6409 = EDGE_LOOP('',(#6410,#6411,#6412,#6413)); +#6410 = ORIENTED_EDGE('',*,*,#6297,.F.); +#6411 = ORIENTED_EDGE('',*,*,#6367,.T.); +#6412 = ORIENTED_EDGE('',*,*,#6337,.T.); +#6413 = ORIENTED_EDGE('',*,*,#6389,.F.); +#6414 = PLANE('',#6415); +#6415 = AXIS2_PLACEMENT_3D('',#6416,#6417,#6418); +#6416 = CARTESIAN_POINT('',(-292.5,568.,180.)); +#6417 = DIRECTION('',(0.,0.,1.)); +#6418 = DIRECTION('',(1.,0.,-0.)); +#6419 = ADVANCED_FACE('',(#6420),#6426,.T.); +#6420 = FACE_BOUND('',#6421,.T.); +#6421 = EDGE_LOOP('',(#6422,#6423,#6424,#6425)); +#6422 = ORIENTED_EDGE('',*,*,#6313,.F.); +#6423 = ORIENTED_EDGE('',*,*,#6374,.T.); +#6424 = ORIENTED_EDGE('',*,*,#6353,.T.); +#6425 = ORIENTED_EDGE('',*,*,#6396,.F.); +#6426 = PLANE('',#6427); +#6427 = AXIS2_PLACEMENT_3D('',#6428,#6429,#6430); +#6428 = CARTESIAN_POINT('',(-292.5,568.,2.12E+03)); +#6429 = DIRECTION('',(0.,0.,1.)); +#6430 = DIRECTION('',(1.,0.,-0.)); +#6431 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6435)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#6432,#6433,#6434)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#6432 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6433 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#6434 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#6435 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6432, + 'distance_accuracy_value','confusion accuracy'); +#6436 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6437,#6439); +#6437 = ( REPRESENTATION_RELATIONSHIP('','',#6280,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6438) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#6438 = ITEM_DEFINED_TRANSFORMATION('','',#11,#47); +#6439 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #6440); +#6440 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('9', + 'NAU03_Interior_Mounting_Plate','',#5,#6275,$); +#6441 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6277)); +ENDSEC; +END-ISO-10303-21; diff --git a/data/examples/qet_split_cabinet/nau03_test_cabinet_split_report.json b/data/examples/qet_split_cabinet/nau03_test_cabinet_split_report.json new file mode 100644 index 0000000..ac341a7 --- /dev/null +++ b/data/examples/qet_split_cabinet/nau03_test_cabinet_split_report.json @@ -0,0 +1,25 @@ +{ + "source_reference": "D:\\downloadWX\\xwechat_files\\wxid_pv577xuccot722_5d4a\\msg\\file\\2026-04\\MCCB CABINET ASS'Y.STEP", + "outputs": { + "fcstd": "D:\\LightWork3D\\data\\examples\\qet_split_cabinet\\nau03_test_cabinet_split.FCStd", + "step": "D:\\LightWork3D\\data\\examples\\qet_split_cabinet\\nau03_test_cabinet_split.step" + }, + "dimensions_mm": { + "width": 750.0, + "depth": 1300.0, + "height_without_roof_base": 2300.0, + "overall_height": 2543.0 + }, + "hideable_parts": [ + "NAU03_Cabinet_Frame", + "NAU03_Left_Side_Panel", + "NAU03_Right_Side_Panel", + "NAU03_Rear_Panel", + "NAU03_Front_Left_Door", + "NAU03_Front_Right_Door", + "NAU03_Top_Roof", + "NAU03_Bottom_Base", + "NAU03_Interior_Mounting_Plate" + ], + "object_count": 9 +} \ No newline at end of file diff --git a/data/examples/qet_split_cabinet/verify_nau03_split_cabinet.py b/data/examples/qet_split_cabinet/verify_nau03_split_cabinet.py new file mode 100644 index 0000000..576fa5a --- /dev/null +++ b/data/examples/qet_split_cabinet/verify_nau03_split_cabinet.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +import json +import os +import sys +from pathlib import Path + + +def _bootstrap_windows_freecad_runtime() -> None: + if os.name != "nt": + return + runtime_json = os.environ.get("QET_FREECAD_RUNTIME_JSON") + if not runtime_json: + runtime_json = os.path.join(os.environ.get("LOCALAPPDATA", ""), "QETDeps", "runtime.json") + if not runtime_json or not os.path.exists(runtime_json): + return + with open(runtime_json, "r", encoding="utf-8-sig") as handle: + runtime = json.load(handle) + roots = [str(item) for item in runtime.get("path_prefix", []) if item] + freecad_root = runtime.get("freecad_root", "") + roots.extend( + [ + os.path.join(freecad_root, "build", "Mod", "Import"), + os.path.join(freecad_root, "build", "Mod", "Part"), + os.path.join(freecad_root, "build", "Mod"), + os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "downlevel"), + ] + ) + for root in roots: + if root and os.path.isdir(root): + try: + os.add_dll_directory(root) + except (AttributeError, OSError): + pass + if root not in sys.path: + sys.path.append(root) + + +_bootstrap_windows_freecad_runtime() + +import FreeCAD as App +import Import + + +OUT_DIR = Path(__file__).resolve().parent +STEP_PATH = Path(os.environ.get("NAU03_SPLIT_CABINET_STEP", OUT_DIR / "nau03_test_cabinet_split.step")) + +EXPECTED_LABELS = { + "NAU03_Cabinet_Frame", + "NAU03_Left_Side_Panel", + "NAU03_Right_Side_Panel", + "NAU03_Rear_Panel", + "NAU03_Front_Left_Door", + "NAU03_Front_Right_Door", + "NAU03_Top_Roof", + "NAU03_Bottom_Base", + "NAU03_Interior_Mounting_Plate", +} + + +def main() -> None: + if not STEP_PATH.exists(): + raise AssertionError(f"missing STEP output: {STEP_PATH}") + + header = STEP_PATH.read_text(encoding="utf-8", errors="ignore")[:256] + if "ISO-10303-21" not in header: + raise AssertionError("STEP output is missing ISO-10303-21 header") + + doc = App.newDocument("verify_nau03_split") + Import.insert(str(STEP_PATH), doc.Name) + doc.recompute() + + shape_objects = [ + obj + for obj in doc.Objects + if hasattr(obj, "Shape") and not obj.Shape.isNull() and len(obj.Shape.Solids) > 0 + ] + labels = {obj.Label for obj in shape_objects} + missing = sorted(EXPECTED_LABELS - labels) + if missing: + raise AssertionError(f"missing expected independently hideable labels: {missing}") + + if len(shape_objects) > 16: + raise AssertionError(f"too many top-level shape objects: {len(shape_objects)}") + + for obj in shape_objects: + if obj.Shape.Volume <= 0: + raise AssertionError(f"{obj.Label} has no positive volume") + + print("verified NAU03 split cabinet STEP") + print("shape_objects=", len(shape_objects)) + print("labels=", sorted(labels)) + + +if __name__ == "__main__": + main() diff --git a/docs/2D-3D交换协议.md b/docs/2D-3D交换协议.md index 3d41b82..5bc4f93 100644 --- a/docs/2D-3D交换协议.md +++ b/docs/2D-3D交换协议.md @@ -162,6 +162,18 @@ QET 在导出时负责: - `device_models`:设备 3D 模型解析结果 - `wires`:导线起点/终点与标注快照 +### 4.2 FreeCAD 侧 v2 校验规则 + +FreeCAD 当前只接受 `schema_version=2.0` 的交换文件。为了避免旧协议字段继续混入新链路,导入时会直接拒绝下列旧结构: + +- 根级 `terminals[]`:端子必须放在 `devices[].terminals[]` +- `devices[].instance_id`:设备实例必须使用 `devices[].device_instance_id` +- `devices[].element_uuid`:设备顶层不再表达单个 2D 符号,成员关系由 `devices[].terminals[].element_uuid` 表达 +- `device_models[].instance_id` 和 `device_models[].element_uuid`:模型只允许按 `device_models[].device_instance_id` 关联 +- `wires[].start_instance_id` / `wires[].end_instance_id`:第一版数据库绑定仍不依赖这两个字段,正式主键仍是 `terminal_uuid`;但在 QET 端 `terminal_uuid` 尚未完全做到端子实例唯一前,FreeCAD 可把它们作为自动布线端点消歧的辅助信息,优先按 `terminal_uuid + element_uuid + device_instance_id` 匹配 3D 工程端子,避免重复 `terminal_uuid` 时误接到其它设备端子。QET 后续把 `terminal_uuid` 修成真正端子实例 UUID 后,这两个字段可以退回诊断/兼容用途。 + +FreeCAD 内部对象属性 `QetInstanceId` 仍可保留,这是 3D 文档内的对象属性名,不等同于 JSON 旧字段 `instance_id`。 + --- ## 5. `cabinet` 结构 @@ -523,7 +535,7 @@ FreeCAD 根据 `resolved_model_path` 的扩展名导入 `.FCStd`,并在导入 ```json { - "schema_version": "1.0", + "schema_version": "2.0", "project_uuid": "string", "generated_at": "2026-05-18T11:00:00+08:00", "instances": [], @@ -555,6 +567,8 @@ FreeCAD 根据 `resolved_model_path` 的扩展名导入 `.FCStd`,并在导入 - `instances[]` 继续表达:某个 2D `element_uuid` 绑定到哪个 3D 设备实例 - `terminals[]` 表达:某个 2D `terminal_uuid` 绑定到哪个 3D 设备实例,以及可选的哪个 3D 端子实例 - 如果当前版本 QET 只消费设备实例级绑定,`terminal_instance_id` 可暂时忽略,但字段命名应保留清晰语义 +- `3d_to_2d.json` 与优化后的 `2d_to_3d.json` 使用同一套命名:设备实例字段统一为 `device_instance_id`,端子实例字段统一为 `terminal_instance_id` +- `3d_to_2d.json` 不再输出旧字段 `instance_id`;QET 读取后再把 `device_instance_id / terminal_instance_id` 写入两张绑定表的 `instance_id` 列 第一版不回写: diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index cf71a47..1d04455 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -805,6 +805,8 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 面板中的 `端子接入警告距离 mm` 用于判断“端子接入过长”。设为 `0` 时按默认规则自动计算;如果当前机柜尺度较大,且 600-700mm 的端子接入属于可接受的设备局部出线,可以把该值调到 700mm 左右再检查。这个参数只影响质量告警,不会放宽 `端子接入最大距离 mm`,也不会让超过最大距离的端子强行接入。 +如果路径网络诊断包含 `unconnected_terminals`,点击 `选择未接入端子`。系统会从最新 `RoutingPathNetwork` 诊断中选择未接入路由网络、或端子出口到最近网络距离超过 `端子接入最大距离 mm` 的端子及所属设备;状态栏会显示本次样例里的最大最近网络距离。选中后先确认设备是否已经装配到柜内正确位置,再看端子附近是否缺线槽入口、过线孔、黄色 `UserPath` 或设备局部出线路径;如果装配和路径都合理,但实际柜型允许更长的局部接入,再考虑调大 `端子接入最大距离 mm`。 + 如果有线槽但导线仍大量走布线面,优先看 `RoutingPathNetwork.QetDiagnosticIssueCodes` 是否包含 `wire_ducts_without_terminal_access / 线槽未接入端子主网络`。这个问题表示线槽已经识别成路径 carrier,但它所在的路径组件没有任何 `TerminalAccess`,导线很难自然进入线槽。中文报告会尽量显示“建议桥接到哪个主网络”和最近距离;`QetDiagnosticJson.wire_ducts_without_terminal_access[].bridge_suggestion` 会保存建议连接的两段 carrier、两个最近点和距离。处理方式是在 FreeCAD 中用 UserPath、线槽开口或桥接路径,把线槽组件接到端子接入所在的主网络,再重新生成布线路径网络和导线。 `生成布线路径网络` 不会把 FreeCAD 的 Origin 坐标轴、已有 `QETRouteCarrier*` 或异常巨大包围盒对象当成用户路径源。真正的 `UserPath` 需要来自你选中的草图线、Draft 线、带 `Points` 的路径对象,或通过 `按诊断建议生成桥接` / `选中两路径生成桥接` 生成。如果 `生成布线连接` 后诊断显示 `路径采用:线槽/主路径 0 条,布线面/辅助路径 N 条`,说明当前导线基本都在走安装板/门板等辅助 RoutingRange,优先补线槽到端子主网络的桥接路径,或手动画柜内主路径后点击 `选中路径作为用户路径`。 @@ -893,12 +895,50 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 如果某条线已经生成但端子附近拉出很长一段斜线或折线,选中该导线对象查看 `QetRouteEntryDistanceMm`、`QetRouteExitDistanceMm`、`QetRouteAccessWarningDistanceMm` 和 `QetRouteAccessStatus`。其中 `LongAccessWarning` 表示起点或终点到主路径网络的接入距离超过当前告警阈值;`QetRouteAccessWarningSides` 会显示触发侧,`entry` 是起点侧,`exit` 是终点侧。出现该提示时,优先检查设备是否已经装配到正确位置、端子局部出线路径是否存在、用户路径或线槽是否离设备端子太远。 +端子默认出线方向来自工程端子 LCS 的本地 `+Z` 方向;如果工程端子带 `QetTerminalExitDirectionJson`,则优先使用该显式方向。该字段使用 FreeCAD 文档坐标,例如 `{"x":1,"y":0,"z":0}` 表示沿全局 X 正方向出线。这个设计对应 SW 中 CPoint/连接点保存出线方向的思路,适合把常用设备模板里的真实出线方向固化下来。 + +如果当前只是端子 CPoint 方向不对、但不需要画完整局部路径,可以直接在工程里设置显式出线方向: + +1. 选中一个可布线工程端子。 +2. 再选中一条表示出线方向的草图线、Draft 线、边或连续 Wire。 +3. 点击 `选中端子设置出线方向`。 +4. 系统会取所选线的第一段方向,归一化后写入该端子的 `QetTerminalExitDirectionJson`,只修改当前 FreeCAD 文档,不写 QET 数据库。 +5. 重新点击 `生成布线路径网络` 或 `生成布线连接`。 + +这个动作只保存“方向”,仍由系统按 `端子出线长度 mm` 和 `端子出线最大长度 mm` 计算出线段;如果端子附近需要梳状、折线或跨平面局部走线,应使用 `选中端子设置局部出线`。 + +如果端子没有显式方向,且默认 LCS 方向会在设备包围盒内走很深才离开设备,系统会尝试自动改用最近的侧向出口,避免第一段导线穿过设备主体或悬空过长。选中导线后查看 `QetRouteDiagnosticsJson.endpoint_access.*_diagnostics.exit_direction_corrected`,为 `true` 表示本次使用了自动校正方向;`original_exit_direction` 是原 LCS 方向,`exit_direction` 是实际采用方向。显式方向不会被自动改,若显式方向错误,应修改设备模板方向、工程端子 `QetTerminalExitDirectionJson`,或给该端子设置局部出线路径。 + +端子默认出线长度仍是 `terminal_exit_length=20mm`,但现在有 `terminal_exit_max_length=80mm` 上限。端子如果位于设备包围盒内部,系统会尝试沿出线方向离开设备外轮廓;如果离开包围盒需要超过上限,出线段会被截断,并在诊断中标记 `terminal_exit_length_capped`。出现这个问题通常表示端子方向朝内、设备包围盒过大,或端子位置放在设备深处;不要简单把上限调得很大,应优先检查端子 LCS 方向、显式出线方向或局部出线路径。 + +选中生成的导线对象后,可以在 `QetRouteDiagnosticsJson.endpoint_access.start_diagnostics / end_diagnostics` 中查看每侧端子的 `exit_rule`、`exit_direction_source`、`exit_direction`、`requested_exit_length_mm`、`actual_exit_length_mm`、`device_exit_required_length_mm` 和 `exit_length_capped`。如果 `exit_rule=local_route`,说明该端子正在使用 `QetTerminalLocalRoutePointsJson` 局部出线路径;如果 `exit_length_capped=true`,说明这侧端子按当前显式方向无法在合理长度内离开设备包围盒,后续容易出现端子附近悬空过长或穿模,应优先修正端子方向或给该端子设置局部出线路径。 + +点击 `检查布线路径网络` 时,也会提前汇总端子出线问题。`corrected_terminal_exits[]` 表示默认 LCS 出线方向被系统自动改到最近侧向出口,通常说明设备模板端子方向还需要复查;`capped_terminal_exits[]` 表示端子按当前显式方向或默认方向无法在最大出线长度内离开设备包围盒,系统已经截断出线段。两个数组都会保留端子名、端子 UUID、父设备、原始方向、实际方向、请求长度、实际长度和上限,便于手动验收时先定位设备端子,再决定是修模板 CPoint、设置工程端子局部出线,还是补主路径入口。 + +如果 `QetTerminalExitDirectionJson` 格式错误、方向向量无法解析或方向长度为 0,路径网络诊断会额外输出 `invalid_terminal_exit_directions[]`。这种情况不会让 FreeCAD 依赖 QET 计算 3D 路径,而是明确提示当前 FreeCAD 文档或设备模板中的 CPoint 方向元数据需要修正;可以用 `选中端子设置出线方向` 重写当前工程端子的显式方向,或回到设备模板中修正后重新导入。 + +如果要直接定位这些端子,点击 `选择出线问题端子`。系统会从最新 `RoutingPathNetwork` 诊断中合并选择 `corrected_terminal_exits[]`、`capped_terminal_exits[]`、`invalid_terminal_exit_directions[]` 和 `invalid_terminal_local_routes[]` 对应的端子及父设备;这个操作只负责定位,不会自动改端子方向或重新布线。选中后先看端子 LCS 朝向、显式 `QetTerminalExitDirectionJson`、局部路径 `QetTerminalLocalRoutePointsJson`、设备包围盒是否过大,再决定是否设置显式出线方向、设置局部出线路径或回到设备模板修正 CPoint。 + +每个自动生成的 `TerminalAccess` carrier 会记录接入目标:`QetTerminalAccessTargetKind / Name / Label / DistanceMm` 表示端子局部出口接到哪条线槽、`UserPath`、过线孔或面板路径;`QetTerminalAccessTargetRule` 表示选择规则,`main_path_nearest` 是直接接入最近主路径,`main_path_preferred_over_fallback` 是附近虽有 `RoutingRange` 等兜底路径但系统仍优先接入主路径,`fallback_only` 表示当前找不到线槽/UserPath/过线孔等主路径,只能退回面板路径或辅助路径。`QetTerminalAccessFallbackTarget=1` 时,应优先补线槽入口、黄色草图 `UserPath`、过线孔或设备局部路径,再重新生成布线路径网络。 + +如果端子已经通过局部出线路径离开设备,但局部出口到主路径入口的短接入段会重新穿过该端子所属设备包围盒,系统会给这段 `TerminalAccess` 自动加一个外侧绕行折点。`QetTerminalAccessAvoidedEndpointDevice=1` 表示这条接入线已经做过端点设备避让;选中最终导线时,也可以在 `QetRouteDiagnosticsJson.network.start_terminal_access_avoided_endpoint_device / end_terminal_access_avoided_endpoint_device` 里看是哪一侧触发。这个规则只处理端子接入主路径前的短段,不替代整条导线的全局碰撞避让。如果避让后仍穿其它设备,仍需要补更合理的 UserPath、线槽入口或设备局部出线路径。 + +点击 `检查布线路径网络` 时,诊断 JSON 也会汇总 `terminal_access_fallback_targets[]` 和 `terminal_access_endpoint_device_avoidance[]`。前者表示某些端子接入只能退回 `RoutingRange` 等兜底路径,通常需要补线槽入口、`UserPath` 或过线孔;后者表示某些端子接入段已经为了避开端点设备做了绕行,后续我进行手动验收时会优先检查这些端子附近是否缺设备局部出线路径或主路径入口。这两个数组都包含端子名、端子 UUID、父设备、`TerminalAccess` 接入段对象名、目标路径类型、目标路径对象名、`access_points[]` 和 `access_length_mm`,便于自动定位对象并判断接入段是否过长、是否绕回设备附近。 + +如果要定位端子接入退回到布线面的对象,点击 `选择端子退回位置`。该按钮既能读取独立 `RoutingPathNetwork.terminal_access_fallback_targets[]`,也能读取批量布线诊断里的端子退回样例;只执行 `检查布线路径网络`、还没有生成导线时,也可以先选中端子、父设备、`TerminalAccess` 接入段和退回目标,判断应该补线槽入口、黄色 `UserPath`、过线孔还是设备局部出线路径。 + +如果这些退回目标只是缺一小段到主路径的入口,可以直接点击 `按诊断建议生成桥接`。该按钮现在既能读取批量布线诊断里的 `terminal_access_fallback_target_samples[]`,也能读取刚执行 `检查布线路径网络` 后生成的 `RoutingPathNetwork.terminal_access_fallback_targets[]`,自动在退回布线面和最近线槽、`UserPath`、过线孔等主路径之间生成 `TerminalAccessFallbackBridge`。生成后重新执行 `生成布线路径网络` 或 `生成布线连接`,端子接入会优先走补出的桥接路径;如果仍然退回布线面,说明需要补更明确的主路径入口或设备局部出线路径。 + +如果要直接定位端点设备避让问题,点击 `选择端点避让接入`。系统会读取最新 `RoutingPathNetwork` 诊断中的 `terminal_access_endpoint_device_avoidance[]`,选中对应端子、父设备、目标主路径和 `TerminalAccess` 接入段;这个按钮主要服务手动验收和开发侧复查,只定位对象,不重新布线、不写 QET 数据库。 + `检查布线路径网络` 和批量布线的 `routing_path_network_diagnostic.long_terminal_accesses[]` 会保留长接入样例。样例里包含 `parent_device_label / parent_device_name`、`terminal_origin`、`terminal_access_points`、`terminal_access_dominant_axis` 和 `terminal_access_axis_lengths_mm`。如果 `terminal_access_dominant_axis` 是 `z`,且 `z` 方向长度占大头,通常表示端子点和柜内主路径平面高度差过大;优先检查该设备装配高度、端子 LCS 方向,或为该设备补局部出线路径。 如果要快速定位这些端子,点击 `选择长接入端子`。系统会从最新批量布线诊断中的 `routing_path_network_diagnostic.long_terminal_accesses[]` 查找端子对象并选中。真实工程中类似 PEN 325-328 这类端子被选中后,可以直接检查它们是否位于异常高度、是否缺设备局部出线路径,或附近是否缺主路径入口。 如果要从设备角度排查,点击 `选择长接入设备`。系统会读取长接入样例里的 `parent_device_name / parent_device_label` 并选中对应设备。通常先用 `选择长接入端子` 看具体端子点,再用 `选择长接入设备` 检查该设备整体是否装配到正确高度、端子 LCS 是否随设备移动,以及设备附近是否需要补局部出线路径。 +这两个长接入定位按钮既能读取批量布线诊断内嵌的 `routing_path_network_diagnostic.long_terminal_accesses[]`,也能直接读取独立 `RoutingPathNetwork` 诊断里的 `long_terminal_accesses[]`。因此只执行 `检查布线路径网络`、还没有生成导线时,也可以先定位长接入端子和设备,适合在正式布线前先修装配高度、端子方向和局部出线路径。 + 如果确认是某个工程端子缺少设备局部出线路径,可以直接在当前装配工程里补: 1. 选中一个可布线工程端子。 @@ -945,6 +985,8 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 `汇总布线诊断` 还会根据当前问题给出下一步建议,例如 `点击“选择缺端子设备”定位需要补工程端子的设备`、`点击“选择异常导线”定位带问题码的导线`、`点击“选择碰撞父装配”确认结构件后再标记忽略碰撞`。手测时可以先看这一行,再决定下一步点哪个定位按钮。 +如果路径网络诊断中存在 `terminal_exit_direction_corrected`、`terminal_exit_length_capped`、`invalid_terminal_exit_directions` 或 `invalid_terminal_local_routes`,汇总建议会提示点击 `选择出线问题端子`。这一步用于先定位设备端子本身的问题,再决定是否修设备模板 CPoint/LCS、重写显式出线方向、设置工程端子局部出线路径,或补端子附近到线槽/UserPath 的入口。 + 如果要判断某根线是明显穿模还是只是距离太近,选中导线对象查看 `QetRouteCollisionStatus`、`QetRouteHardIntersectionCount` 和 `QetRouteClearanceWarningCount`。`HardIntersectionWarning` 表示导线穿过障碍包围盒,应优先改路径或设备位置;`ClearanceWarning` 表示导线没有穿过障碍,但低于安全间隙,通常需要微调路径或安全间隙参数。 批量诊断中的 `collision_samples[]` 也会带 `wire_object_label`。如果报告出现“碰撞示例”,可以先复制这个 Label 到树目录中查找对应导线,再结合 `collision_kind` 判断是硬碰撞还是安全间隙。碰撞样例还会带 `obstacle_parent_labels / obstacle_parent_names`,用于判断类似 `NAUO141` 这样的零件属于前门、柜体、安装板还是具体设备;确认是装配辅助件或可穿过结构后,再手动标记为忽略碰撞对象。 diff --git a/docs/FreeCAD 端子显示连线保存回写开发文档.md b/docs/FreeCAD 端子显示连线保存回写开发文档.md index e69117e..9f85030 100644 --- a/docs/FreeCAD 端子显示连线保存回写开发文档.md +++ b/docs/FreeCAD 端子显示连线保存回写开发文档.md @@ -377,7 +377,9 @@ ManualWiring.py - `QET_Template_AddTerminal` - `QET_Template_SaveAsFCStd` - 第一版端子位置可以通过用户选择对象/点位后的三维坐标生成。 -- 第一版端子方向默认使用单位旋转,后续再补出线方向编辑。 +- 第一版端子方向以模板 LCS 的本地 `+Z` 作为默认出线方向;工程端子也允许保存 `QetTerminalExitDirectionJson`,用文档坐标明确指定出线方向,例如 `{"x":1,"y":0,"z":0}`。这相当于 SW Electrical 3D 的 CPoint 方向数据,适合沉淀到常用设备模板。 +- 如果没有显式出线方向,FreeCAD 侧会在端子位于设备包围盒内部且默认 LCS 方向需要很长距离才能离开设备时,自动尝试改用最近的侧向出口;显式方向不自动改,只按出线长度上限诊断,避免覆盖模板作者或人工指定的 CPoint 方向。 +- 当前工程可通过 `QetTerminalLocalRoutePointsJson` 保存端子局部出线路径;一旦存在局部路径,自动布线优先使用它连接到 TerminalAccess 和柜内主路径网络,不再按默认 LCS 出线段生成。 模板端子属性: @@ -616,4 +618,8 @@ ManualWiring.py - 2026-05-20:新增 `TemplateAuthoringPanel.py`,提供“设备模板端子制作”任务面板和 `QET_Template_OpenAuthoringPanel` 命令;面板支持输入端子名、添加端子、校验端子、保存 FCStd,并已同步到运行目录验证模块可导入。 - 2026-05-25:新增 `WiringImport.py`,把 `2d_to_3d.json` 中的 `wires` 导入为 `QETWiring_01_Tasks` 下的导线任务;`ExchangeBootstrap.py` 已接入启动导入流程。`ManualWiringPanel.py` 增加任务列表、选择导线任务和删除最后折点,按任务生成导线时会把 `wire_id / net_uuid / group_uuid / wire_mark` 写入正式导线对象,并把任务状态更新为 `Routed`。已通过 35 项 `freecad_exchange*_test.py` 单元测试,并安装到 `D:\fc\run-FreeCAD-1.1.1` 运行目录验证 `WiringImport / ManualWiring / ManualWiringPanel / WiringObjects` 可导入。 - 2026-05-25:修复 FCStd 设备导入后模板 LCS 留在工程场景的问题;导入时会把模板槽位位置和朝向缓存到设备组 `QetTemplateSlotsJson`,随后删除模板 LCS 及其 `OriginFeatures`,工程端子仍按 `terminal_uuid` 生成到 `QETTerminals_*`。已补单元测试验证 FCStd 导入不保留模板 LCS、切回 STEP 会清空旧槽位缓存,并避免重复访问已删除对象的 `Group / InList / Name`。 +- 2026-06-15:新增 QET 待装配设备导入策略。FreeCAD 从 QET 打开项目时,新设备默认只创建 `QETDevice_*` 设备组并标记 `QetAssemblyState=Pending`,不再自动导入几何到原点;已装配设备如果在 `scene.FCStd` 中已有模型对象,则继续复用并标记 `Placed`。新增 `list_pending_devices()` 后端清单和 `QET_Exchange_InsertPendingDevice` 命令,用户可选中整个 `QETDevice_*` 设备组执行“插入待装配设备”,导入模型后状态转为 `Placed`。已用 `freecad_exchange_device_import_fcstd_test.py` 验证默认 pending、显式插入、命令注册和清单粒度只返回整设备组,不返回 `JHD5-6灰001` 等内部几何子对象。待跟进:补专用待装配设备任务面板或树右键菜单。 +- 2026-06-15:新增 `PendingDeviceAssemblyPanel.py` 待装配设备任务面板,提供“刷新清单”“插入设备”“插入到选中目标”三个入口。面板清单按 `QETDevice_*` 整设备显示,用户可先在 3D 视图选择安装板、导轨、线槽或柜体面,再把设备插入到目标对象;如果 FreeCAD 提供选中面的拾取点或面中心点,则优先用该点作为设备 Placement,否则退回目标对象 Placement。`insert_pending_device()` 新增 `mount_target / mount_placement` 参数并写入 `QetMountMode / QetMountHostName / QetMountHostLabel / QetMountHostKind`,保存 `scene.FCStd` 后可作为装配状态恢复依据。已用单元测试验证插入到安装目标、显式安装点优先、面板命令注册和清单显示。 +- 2026-06-15:补强待装配设备贴合语义。`insert_pending_device()` 新增 `mount_normal / mount_offset_mm`,可按选中面的法向应用贴合间距,并把 `QetMountHostNormalJson / QetMountOffsetMm` 写入设备组;待装配面板新增“贴合间距”输入,选中面时会尽量读取面法向,插入到目标时传给后端。已用单元测试验证设备 Placement 会沿法向偏移,且法向和间距元数据被保存。 +- 2026-06-15:新增 `tests/manual/freecad_pending_device_scene_smoke.py`,用真实 `FreeCADCmd.exe` 创建设备模型和 `QETScene.FCStd`,执行待装配设备插入、保存、关闭、重新打开,并断言设备仍为 `Placed`、Placement 为 `(10,20,35)`、挂载目标和法向/间距元数据仍存在。已在 `D:\fc\run-FreeCAD-1.1.1\bin\FreeCADCmd.exe` 下执行通过。 ``` diff --git a/docs/superpowers/plans/2026-06-17-freecad-terminal-access-routing.md b/docs/superpowers/plans/2026-06-17-freecad-terminal-access-routing.md new file mode 100644 index 0000000..9e9eea6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-17-freecad-terminal-access-routing.md @@ -0,0 +1,128 @@ +# FreeCAD Terminal Access Routing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 参考 SOLIDWORKS Electrical 3D 的 CPoint/RPoint 思路,把 FreeCAD 自动布线的端子出线方向、出线长度和 TerminalAccess 接入路径做成可诊断、可测试、可手动验收的工程规则。 + +**Architecture:** 第一阶段保留现有 `RoutingNetwork.py` 作为路径网络核心,但把端子出线规则集中到少量函数:显式出线方向优先,其次 LCS 方向,再通过设备包围盒和候选方向做校正。`AutoRouting.py` 继续消费 `terminal_access_path_points_with_network_access()`,诊断数据写入 route/report,面板和文档读取这些字段。 + +**Tech Stack:** FreeCAD Python API, `src/Mod/FreeCADExchange`, `unittest`, `FreeCADCmd.exe` + +--- + +### Task 1: 出线长度上限和方向诊断 + +**Files:** +- Modify: `src/Mod/FreeCADExchange/RoutingNetwork.py` +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` + +- [x] **Step 1: Write failing tests** + +Add tests that create a terminal inside a large device box and assert that the default access path does not extend indefinitely through the full box. Add a second test that verifies the access diagnostics include the selected direction, requested exit length, actual exit length, and whether the length was capped. + +- [x] **Step 2: Run focused tests** + +Run: + +```powershell +python -B -m unittest tests.python.freecad_exchange_auto_routing_test.AutoRoutingTest +``` + +Expected before implementation: the new tests fail because current bbox fallback can return a long exit segment and route diagnostics do not expose capped length metadata. + +- [x] **Step 3: Implement minimal routing rule** + +Add `terminal_exit_max_length` option with a conservative default. The device-aware exit point should cap bbox-derived length and record diagnostics when the cap is reached. Keep Chinese comments near non-obvious engineering rules. + +- [x] **Step 4: Verify** + +Run the same focused tests. Expected: PASS. + +### Task 2: 显式端子出线方向 + +**Files:** +- Modify: `src/Mod/FreeCADExchange/TerminalObjects.py` +- Modify: `src/Mod/FreeCADExchange/RoutingNetwork.py` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` + +- [x] **Step 1: Write failing tests** + +Add tests that set a terminal property such as `QetTerminalExitDirectionJson={"x":1,"y":0,"z":0}` and verify the access path uses that vector instead of LCS `+Z`. + +- [x] **Step 2: Implement explicit direction reader** + +Add a small helper that reads explicit direction JSON or comma-separated text, normalizes it, and falls back to current LCS direction when missing or invalid. + +- [x] **Step 3: Verify** + +Run the focused tests and full auto-routing test module. + +### Task 3: TerminalAccess 接入主路径质量 + +**Files:** +- Modify: `src/Mod/FreeCADExchange/RoutingNetwork.py` +- Modify: `src/Mod/FreeCADExchange/AutoRouting.py` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` + +- [x] **Step 1: Write failing tests** + +Add tests where both a main UserPath/WireDuct and a fallback RoutingRange exist. Assert TerminalAccess prefers the main path unless outside max distance. + +- [x] **Step 2: Improve target selection diagnostics** + +Expose target kind, target label, distance, primary segment count, and fallback reason in route diagnostics. + +- [x] **Step 3: Verify** + +Run focused and full routing tests. + +### Task 4: 文档和运行目录同步 + +**Files:** +- Modify: `docs/FreeCAD 机柜装配操作文档.md` +- Modify: `docs/FreeCAD 端子显示连线保存回写开发文档.md` +- Runtime copy: `D:\fc\run-FreeCAD-1.1.1\Mod\FreeCADExchange` + +- [x] **Step 1: Update Chinese docs** + +Document terminal exit direction, explicit direction property, exit length cap, TerminalAccess target metadata, and manual testing steps. + +- [x] **Step 2: Sync runtime plugin** + +Run: + +```powershell +robocopy D:\LightWork3D\src\Mod\FreeCADExchange D:\fc\run-FreeCAD-1.1.1\Mod\FreeCADExchange /E /NFL /NDL /NJH /NJS /NP +``` + +Robocopy return code 0 or 1 is acceptable. + +- [x] **Step 3: FreeCADCmd verification** + +Run: + +```powershell +D:\fc\run-FreeCAD-1.1.1\bin\FreeCADCmd.exe -c "import sys; sys.path.insert(0, r'D:\fc\run-FreeCAD-1.1.1\Mod\FreeCADExchange'); import AutoRouting, RoutingNetwork, TerminalObjects; print('freecad_exchange_import_ok')" +``` + +Expected: `freecad_exchange_import_ok`. + +### Task 5: TerminalAccess 端点设备避让 + +**Files:** +- Modify: `src/Mod/FreeCADExchange/RoutingNetwork.py` +- Test: `tests/python/freecad_exchange_auto_routing_test.py` +- Docs: `docs/FreeCAD 机柜装配操作文档.md` + +- [x] **Step 1: Write failing test** + +Add a local-route terminal case where the short TerminalAccess segment from the local route exit to a UserPath would re-enter the terminal parent device bbox. + +- [x] **Step 2: Implement endpoint-device avoidance** + +When generating TerminalAccess carriers, test only the short access-to-target segment against the terminal parent bbox. If the direct orthogonal path crosses that bbox, choose the shortest dogleg candidate outside the bbox and mark `QetTerminalAccessAvoidedEndpointDevice=1`. + +- [x] **Step 3: Verify and document** + +Run the focused TerminalAccess tests and document the manual-test property. diff --git a/docs/数据库设计.md b/docs/数据库设计.md index 9c45b58..998b34f 100644 --- a/docs/数据库设计.md +++ b/docs/数据库设计.md @@ -425,30 +425,74 @@ QET 侧建议保留并改造一个工具项: ### 15.1 `2d_to_3d.json` -第一版只要求包含最小绑定信息: +第一版交换文件使用优化后的 `schema_version=2.0` 快照结构。 -- 设备绑定: - - `project_uuid` - - `element_uuid` - - `instance_id` -- 端子绑定: +`2d_to_3d.json` 不再把设备顶层当成单个 `element_uuid` 镜像,而是按 3D 设备实例组织: + +- 顶层: - `project_uuid` - - `terminal_uuid` - - `instance_id` + - `devices[]` + - `device_models[]` + - `wires[]` +- 设备实例: + - `devices[].device_instance_id` + - `devices[].display_tag` + - `devices[].terminals[]` +- 设备端子: + - `devices[].terminals[].terminal_uuid` + - `devices[].terminals[].element_uuid` + - `devices[].terminals[].terminal_instance_id` +- 设备模型: + - `device_models[].device_instance_id` + - `device_models[].resolved_model_path` 说明: -- `instance_id` 在第一版中由 FreeCAD 侧生成更合理 -- 如果首次进入 3D 时尚未生成 `instance_id`,可以先导出为空,再由 FreeCAD 创建后回写 +- JSON 协议中用 `device_instance_id` 表达 3D 设备实例,避免和端子实例混用同一个字段名 +- JSON 协议中用 `terminal_instance_id` 表达 3D 端子对象实例 +- 数据库绑定表列名仍然保持 `instance_id`,由 QET 在读取 JSON 时把 `device_instance_id / terminal_instance_id` 映射进去 ### 15.2 `3d_to_2d.json` -第一版只建议回写: +`3d_to_2d.json` 文件名保持不变,但字段名同步为优化后的 2D/3D 交换协议。 + +推荐结构: + +```json +{ + "schema_version": "2.0", + "project_uuid": "string", + "generated_at": "2026-05-18T11:00:00+08:00", + "instances": [ + { + "element_uuid": "string", + "device_instance_id": "string" + } + ], + "terminals": [ + { + "terminal_uuid": "string", + "device_instance_id": "string", + "terminal_instance_id": "string" + } + ] +} +``` + +当前只建议回写: - `project_uuid` - `element_uuid` -- `instance_id` +- `device_instance_id` - `terminal_uuid` +- `terminal_instance_id` + +说明: + +- `3d_to_2d.json` 不再输出旧字段 `instance_id` +- QET 读取 `instances[].device_instance_id` 后写入 `project_2d3d_symbol_binding.instance_id` +- QET 读取 `terminals[].terminal_instance_id` 后写入 `project_2d3d_terminal_binding.instance_id` +- `terminals[].device_instance_id` 用于说明该端子属于哪个 3D 设备实例,便于校验和排障 当前不要求回写: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 204c93d..8026b8f 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -31,6 +31,8 @@ LOCAL_ACCESS_DETOUR_CLEARANCE = 10.0 DEFAULT_OPTIONS = { # 端子出来先走一小段,避免导线贴着设备外壳起步。 "terminal_exit_length": 20.0, + # 设备包围盒很大或端子方向朝内时,不允许默认出线无限延长;超限会写入诊断。 + "terminal_exit_max_length": 80.0, "lane_axis": "auto", "lane_spacing": 10.0, "lane_max_offset": 30.0, @@ -103,10 +105,13 @@ DEFAULT_OPTIONS = { "preflight_routeability_sample_limit": 0, # 自动布线时如果诊断能明确建议“线槽组件 -> 端子主网络”的桥接点, # 先生成 UserPath 桥再布线,避免真实工程长期退回 RoutingRange 兜底。 - "auto_create_diagnostic_bridges": False, + "auto_create_diagnostic_bridges": True, # 第一次布线若发现“兜底区域 -> 当前主路径”的缺主路径绕行配对, # 自动补一段 UserPath 桥并重跑一次,让少量剩余碰撞线回到主路径网络。 - "auto_create_main_path_detour_bridges": False, + "auto_create_main_path_detour_bridges": True, + # 第一次布线若发现端子接入退回布线面/辅助路径, + # 自动补一段到最近主路径的 UserPath 桥并重跑一次。 + "auto_create_terminal_access_fallback_bridges": True, } @@ -324,6 +329,44 @@ def _auto_lane_axis(route_points): return "x" +def _dominant_route_axis(route_points): + points = [_vector(point) for point in route_points or []] + if len(points) < 2: + return "" + extents = {"x": 0.0, "y": 0.0, "z": 0.0} + for index in range(len(points) - 1): + start = points[index] + end = points[index + 1] + extents["x"] += abs(float(end.x) - float(start.x)) + extents["y"] += abs(float(end.y) - float(start.y)) + extents["z"] += abs(float(end.z) - float(start.z)) + dominant_axis = max(extents, key=lambda axis: extents[axis]) + if extents[dominant_axis] <= 0.000001: + return "" + return dominant_axis + + +def _secondary_lane_axis(route_points, primary_axis): + dominant_axis = _dominant_route_axis(route_points) + for axis in ("z", "y", "x"): + if axis != primary_axis and axis != dominant_axis: + return axis + for axis in ("z", "y", "x"): + if axis != primary_axis: + return axis + return "" + + +def _lane_offset_for_order(lane_order, lane_direction, lane_spacing, max_offset): + primary_offset = float(lane_order) * lane_spacing * lane_direction + secondary_order = 0 + if max_offset > 0.0 and abs(primary_offset) > max_offset: + max_primary_order = max(int(math.floor(max_offset / lane_spacing)), 1) if lane_spacing > 0.0 else 1 + secondary_order = max(lane_order - max_primary_order, 0) + primary_offset = max_offset if primary_offset > 0.0 else -max_offset + return primary_offset, secondary_order + + def _lane_payload(route_index, options, route_points=None): opts = options or {} lane_axis = (opts.get("lane_axis") or "y").lower() @@ -335,32 +378,117 @@ def _lane_payload(route_index, options, route_points=None): lane_spacing = float(opts.get("lane_spacing", 0.0) or 0.0) if lane_index <= 0: lane_offset = 0.0 + secondary_axis = "" + secondary_offset = 0.0 else: lane_order = (lane_index + 1) // 2 lane_direction = 1.0 if lane_index % 2 == 1 else -1.0 - lane_offset = float(lane_order) * lane_spacing * lane_direction - # 多根线共路时 lane 序号可能很大;限制显示偏移,避免把线推到柜体或线槽外。 max_offset = float(opts.get("lane_max_offset", 0.0) or 0.0) - if max_offset > 0.0 and abs(lane_offset) > max_offset: - lane_offset = max_offset if lane_offset > 0.0 else -max_offset - return { + lane_offset, secondary_order = _lane_offset_for_order( + lane_order, + lane_direction, + lane_spacing, + max_offset, + ) + secondary_axis = "" + secondary_offset = 0.0 + if secondary_order > 0 and lane_spacing > 0.0: + secondary_axis = _secondary_lane_axis(route_points, lane_axis) + secondary_direction = 1.0 if secondary_order % 2 == 1 else -1.0 + secondary_magnitude = ((secondary_order + 1) // 2) * lane_spacing + if max_offset > 0.0 and secondary_magnitude > max_offset: + secondary_magnitude = max_offset + secondary_offset = secondary_magnitude * secondary_direction + payload = { "index": lane_index, "axis": lane_axis, "spacing_mm": lane_spacing, "max_offset_mm": float(opts.get("lane_max_offset", 0.0) or 0.0), "offset_mm": lane_offset, } + if secondary_axis and abs(secondary_offset) > 0.000001: + # 主方向达到上限后,使用第二方向继续错开密集导线,避免多根线在同一路径上完全重合。 + payload["secondary_axis"] = secondary_axis + payload["secondary_offset_mm"] = secondary_offset + return payload def _apply_lane_offset(points, lane): offset = float((lane or {}).get("offset_mm", 0.0) or 0.0) - if abs(offset) <= 0.000001: + secondary_offset = float((lane or {}).get("secondary_offset_mm", 0.0) or 0.0) + if abs(offset) <= 0.000001 and abs(secondary_offset) <= 0.000001: return list(points or []) axis = (lane or {}).get("axis", "y") - return [ - _with_axis(point, axis, _axis_value(point, axis) + offset) - for point in list(points or []) - ] + secondary_axis = (lane or {}).get("secondary_axis", "") + result = [] + for point in list(points or []): + shifted = point + if abs(offset) > 0.000001: + shifted = _with_axis(shifted, axis, _axis_value(shifted, axis) + offset) + if secondary_axis in {"x", "y", "z"} and abs(secondary_offset) > 0.000001: + shifted = _with_axis( + shifted, + secondary_axis, + _axis_value(shifted, secondary_axis) + secondary_offset, + ) + result.append(shifted) + return result + + +def _lane_payload_boundary_aware( + route_index, + options, + route_points=None, + boundary_violation_count=None, + obstacle_hit_count=None, +): + lane = _lane_payload(route_index, options, route_points=route_points) + opts = options or {} + has_boundary_score = callable(boundary_violation_count) + has_obstacle_score = callable(obstacle_hit_count) + if ( + str(opts.get("lane_axis", "auto") or "auto").lower() != "auto" + or abs(float(lane.get("offset_mm", 0.0) or 0.0)) <= 0.000001 + or (not has_boundary_score and not has_obstacle_score) + ): + return lane + + candidates = [] + current_axis = str(lane.get("axis", "") or "").strip() or "y" + current_offset = float(lane.get("offset_mm", 0.0) or 0.0) + current_points = _apply_lane_offset(route_points, lane) + current_obstacle_score = int(obstacle_hit_count(current_points) or 0) if has_obstacle_score else 0 + offsets = (current_offset, -current_offset) if current_obstacle_score > 0 else (current_offset,) + seen = set() + for axis in (current_axis, "x", "y", "z"): + if axis not in {"x", "y", "z"}: + continue + for offset in offsets: + key = (axis, round(float(offset), 6)) + if key in seen: + continue + seen.add(key) + candidate = dict(lane) + candidate["axis"] = axis + candidate["offset_mm"] = float(offset) + points = _apply_lane_offset(route_points, candidate) + boundary_score = int(boundary_violation_count(points) or 0) if has_boundary_score else 0 + obstacle_score = int(obstacle_hit_count(points) or 0) if has_obstacle_score else 0 + candidates.append((boundary_score, obstacle_score, axis, float(offset), candidate)) + if not candidates: + return lane + # lane 自动轴选择优先留在柜内、避开设备;分数相同时保持原先的轴和方向,减少既有工程变化。 + dominant_axis = _dominant_route_axis(route_points) + candidates.sort( + key=lambda item: ( + item[0], + item[1], + 1 if item[2] == dominant_axis else 0, + 0 if item[2] == current_axis else 1, + 0 if abs(item[3] - current_offset) <= 0.000001 else 1, + ) + ) + return candidates[0][4] def _orthogonal_axis_order(start_point, end_point, preferred_axis=None): @@ -787,8 +915,18 @@ def _project_uuid(doc, start_terminal=None, end_terminal=None): def index_terminals(doc): """Return {terminal_uuid: terminal_object} for routable engineering terminals.""" + indexed = {} + for terminal in _collect_routable_terminals(doc): + terminal_uuid = (getattr(terminal, "QetTerminalUuid", "") or "").strip() + if terminal_uuid and terminal_uuid not in indexed: + indexed[terminal_uuid] = terminal + return indexed + + +def _collect_routable_terminals(doc): + """Return routable engineering terminals, preserving duplicate QET terminal UUIDs.""" if doc is None: - return {} + return [] terminals = [] root = None @@ -805,12 +943,112 @@ def index_terminals(doc): if TerminalObjects.is_terminal_object(obj) ) - indexed = {} + unique = [] + seen = set() for terminal in terminals: - terminal_uuid = (getattr(terminal, "QetTerminalUuid", "") or "").strip() - if terminal_uuid and terminal_uuid not in indexed: - indexed[terminal_uuid] = terminal - return indexed + marker = id(terminal) + if marker in seen: + continue + seen.add(marker) + unique.append(terminal) + return unique + + +def _terminal_endpoint_value(terminal, property_name): + return str(getattr(terminal, property_name, "") or "").strip() + + +def _terminal_uuid_duplicate_summary(terminal_candidates, limit=8): + counts = {} + samples = [] + for terminal in list(terminal_candidates or []): + terminal_uuid = _terminal_endpoint_value(terminal, "QetTerminalUuid") + if not terminal_uuid: + continue + counts[terminal_uuid] = counts.get(terminal_uuid, 0) + 1 + for terminal_uuid, count in sorted(counts.items()): + if count <= 1: + continue + if len(samples) < limit: + samples.append({"terminal_uuid": terminal_uuid, "count": count}) + return { + "duplicate_terminal_uuid_count": sum(1 for count in counts.values() if count > 1), + "duplicate_terminal_uuid_samples": samples, + } + + +def _terminal_endpoint_match(terminal_candidates, item, side, allow_single_fallback=True): + terminal_uuid = _wire_item_value(item, "{0}_terminal_uuid".format(side)) + result = { + "terminal": None, + "terminal_uuid": terminal_uuid, + "candidate_count": 0, + "context_match_count": 0, + "ambiguous": False, + "reason_code": "", + } + if not terminal_uuid: + result["reason_code"] = "missing_terminal_uuid" + return result + candidates = [ + terminal + for terminal in list(terminal_candidates or []) + if _terminal_endpoint_value(terminal, "QetTerminalUuid") == terminal_uuid + ] + result["candidate_count"] = len(candidates) + if not candidates: + result["reason_code"] = "terminal_uuid_not_found" + return result + + expected_instance_id = _wire_item_value(item, "{0}_instance_id".format(side)) + expected_element_uuid = _wire_item_value(item, "{0}_element_uuid".format(side)) + if not expected_instance_id and not expected_element_uuid: + if len(candidates) == 1 and allow_single_fallback: + result["terminal"] = candidates[0] + return result + result["ambiguous"] = len(candidates) > 1 + result["reason_code"] = "ambiguous_terminal_uuid" + return result + + matched = [] + for terminal in candidates: + instance_match = ( + expected_instance_id + and _terminal_endpoint_value(terminal, "QetInstanceId") == expected_instance_id + ) + element_match = ( + expected_element_uuid + and _terminal_endpoint_value(terminal, "QetElementUuid") == expected_element_uuid + ) + if instance_match or element_match: + score = (4 if instance_match else 0) + (2 if element_match else 0) + matched.append((score, terminal)) + result["context_match_count"] = len(matched) + if not matched: + if allow_single_fallback and len(candidates) == 1: + result["terminal"] = candidates[0] + return result + result["reason_code"] = "terminal_uuid_not_in_endpoint_context" + return result + # 同名 terminal_uuid 在真实 v2 快照里可能重复;优先命中同一 3D 实例,再看 2D 设备。 + matched.sort(key=lambda pair: pair[0], reverse=True) + best_score = matched[0][0] + best_matches = [terminal for score, terminal in matched if score == best_score] + if len(best_matches) > 1: + result["ambiguous"] = True + result["reason_code"] = "ambiguous_terminal_uuid_context" + return result + result["terminal"] = best_matches[0] + return result + + +def _matching_terminal_for_wire_endpoint(terminal_candidates, item, side, allow_single_fallback=True): + return _terminal_endpoint_match( + terminal_candidates, + item, + side, + allow_single_fallback=allow_single_fallback, + ).get("terminal") def _terminal_element_summary(terminals, element_uuid, limit=5): @@ -825,15 +1063,21 @@ def _terminal_property_summary(terminals, property_name, expected_value, limit=5 expected = str(expected_value or "").strip() if not expected: return {"count": 0, "samples": []} + terminal_values = ( + list((terminals or {}).values()) + if isinstance(terminals, dict) + else list(terminals or []) + ) samples = [] count = 0 - for terminal_uuid, terminal in (terminals or {}).items(): + for terminal in terminal_values: terminal_value = str(getattr(terminal, property_name, "") or "").strip() if terminal_value != expected: continue count += 1 if len(samples) >= limit: continue + terminal_uuid = _terminal_endpoint_value(terminal, "QetTerminalUuid") samples.append( { "terminal_uuid": str(terminal_uuid or "").strip(), @@ -853,6 +1097,8 @@ _MISSING_ENDPOINT_REASON_LABELS = { "no_3d_terminals_for_element": "该 2D 设备在 FreeCAD 中没有工程端子", "no_3d_terminals_for_instance": "该 3D 实例在 FreeCAD 中没有工程端子", "terminal_uuid_not_in_element": "同设备存在端子,但没有匹配该 terminal_uuid", + "ambiguous_terminal_uuid": "terminal_uuid 重复且导线端点缺少设备上下文,无法安全选择端子", + "ambiguous_terminal_uuid_context": "terminal_uuid 重复且设备上下文仍匹配到多个端子,无法安全选择端子", } @@ -860,6 +1106,9 @@ def _missing_endpoint_reason_code(sample, side): terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() if not terminal_uuid: return "missing_terminal_uuid" + match_reason = str(sample.get("{0}_terminal_match_reason_code".format(side), "") or "").strip() + if match_reason in ("ambiguous_terminal_uuid", "ambiguous_terminal_uuid_context"): + return match_reason element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() instance_id = str(sample.get("{0}_instance_id".format(side), "") or "").strip() if not element_uuid and not instance_id: @@ -1017,14 +1266,17 @@ def _wire_endpoint_entries(payload): terminal_uuid = _wire_item_value(item, "{0}_terminal_uuid".format(prefix)) if not terminal_uuid or TerminalObjects.is_local_terminal_uuid(terminal_uuid): continue - if terminal_uuid in seen: + element_uuid = _wire_item_value(item, "{0}_element_uuid".format(prefix)) + instance_id = _wire_item_value(item, "{0}_instance_id".format(prefix)) + endpoint_key = (terminal_uuid, element_uuid, instance_id) + if endpoint_key in seen: continue - seen.add(terminal_uuid) + seen.add(endpoint_key) entries.append( { "terminal_uuid": terminal_uuid, - "element_uuid": _wire_item_value(item, "{0}_element_uuid".format(prefix)), - "instance_id": _wire_item_value(item, "{0}_instance_id".format(prefix)), + "element_uuid": element_uuid, + "instance_id": instance_id, "terminal_display": _wire_item_value( item, "{0}_terminal_display".format(prefix), @@ -1053,12 +1305,21 @@ def _bind_wire_task_terminals(doc, payload): except Exception: project_uuid = "" - indexed = index_terminals(doc) used_objects = set() used_slot_tokens = set() for entry in _wire_endpoint_entries(payload): terminal_uuid = entry["terminal_uuid"] - if terminal_uuid in indexed: + endpoint_item = { + "start_terminal_uuid": terminal_uuid, + "start_element_uuid": entry.get("element_uuid", ""), + "start_instance_id": entry.get("instance_id", ""), + } + if _matching_terminal_for_wire_endpoint( + _collect_routable_terminals(doc), + endpoint_item, + "start", + allow_single_fallback=False, + ) is not None: continue device_group = _device_group_for_wire_endpoint( @@ -1440,6 +1701,15 @@ def _route_issue_codes(route_data, collisions): "collision_warnings", _route_collision_payload(collisions).get("collision_count", 0) > 0, ) + collision_payload = _route_collision_payload(collisions) + append_once( + "hard_intersections", + _safe_int(collision_payload.get("hard_intersection_count", 0)) > 0, + ) + append_once( + "clearance_warnings", + _safe_int(collision_payload.get("clearance_warning_count", 0)) > 0, + ) relation_counts = {} for collision in list(collisions or []): if not isinstance(collision, dict): @@ -1479,6 +1749,14 @@ def _route_issue_codes(route_data, collisions): except Exception: obstacle_hits = 0 append_once("route_candidate_obstacle_hits", obstacle_hits > 0) + terminal_access_target_kinds = { + str(network.get("start_terminal_access_target_kind", "") or "").strip(), + str(network.get("end_terminal_access_target_kind", "") or "").strip(), + } + append_once( + "terminal_access_fallback_targets", + bool(terminal_access_target_kinds.intersection({"RoutingRange", "AuxiliaryPath"})), + ) return codes @@ -1839,8 +2117,29 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non ) exit_length = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) - start_access_points = RoutingNetwork.terminal_access_path_points(start_terminal, exit_length) - end_access_points = RoutingNetwork.terminal_access_path_points(end_terminal, exit_length) + max_exit_length = max(float(opts.get("terminal_exit_max_length", 0.0) or 0.0), 0.0) + start_access_points = RoutingNetwork.terminal_access_path_points_with_network_access( + start_terminal, + exit_length, + max_exit_length=max_exit_length, + ) + end_access_points = RoutingNetwork.terminal_access_path_points_with_network_access( + end_terminal, + exit_length, + max_exit_length=max_exit_length, + ) + start_access_diagnostics = RoutingNetwork.terminal_access_diagnostics( + start_terminal, + exit_length=exit_length, + max_exit_length=max_exit_length, + ) + end_access_diagnostics = RoutingNetwork.terminal_access_diagnostics( + end_terminal, + exit_length=exit_length, + max_exit_length=max_exit_length, + ) + start_terminal_access_carrier = RoutingNetwork.terminal_access_carrier_for_terminal(start_terminal) + end_terminal_access_carrier = RoutingNetwork.terminal_access_carrier_for_terminal(end_terminal) start_origin = start_access_points[0] if start_access_points else _terminal_origin(start_terminal) end_origin = end_access_points[0] if end_access_points else _terminal_origin(end_terminal) start_exit = start_access_points[-1] if start_access_points else _offset(start_origin, _terminal_direction(start_terminal), exit_length) @@ -1898,7 +2197,13 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non carrier_points = RoutingNetwork.path_points(network, path_keys) if not carrier_points: return None - lane = _lane_payload(route_index, opts, route_points=carrier_points) + lane = _lane_payload_boundary_aware( + route_index, + opts, + route_points=carrier_points, + boundary_violation_count=route_boundary_violation_count, + obstacle_hit_count=route_obstacle_hit_count, + ) carrier_points = _apply_lane_offset(carrier_points, lane) points = [] @@ -1929,6 +2234,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "endpoint_access": { "start_points": [_point_payload(point) for point in start_access_points or []], "end_points": [_point_payload(point) for point in end_access_points or []], + "start_diagnostics": start_access_diagnostics, + "end_diagnostics": end_access_diagnostics, }, "network": { "carriers": int(network.get("carrier_count", 0)), @@ -1947,6 +2254,90 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "terminal_access_warning_distance": float( opts.get("terminal_access_warning_distance", 0.0) or 0.0 ), + "start_terminal_access_consumed": start_terminal_access_carrier is not None, + "end_terminal_access_consumed": end_terminal_access_carrier is not None, + "start_terminal_access_carrier": getattr(start_terminal_access_carrier, "Name", ""), + "end_terminal_access_carrier": getattr(end_terminal_access_carrier, "Name", ""), + "start_terminal_access_label": getattr(start_terminal_access_carrier, "Label", ""), + "end_terminal_access_label": getattr(end_terminal_access_carrier, "Label", ""), + "start_terminal_access_target_kind": getattr( + start_terminal_access_carrier, + "QetTerminalAccessTargetKind", + "", + ), + "end_terminal_access_target_kind": getattr( + end_terminal_access_carrier, + "QetTerminalAccessTargetKind", + "", + ), + "start_terminal_access_target_name": getattr( + start_terminal_access_carrier, + "QetTerminalAccessTargetName", + "", + ), + "end_terminal_access_target_name": getattr( + end_terminal_access_carrier, + "QetTerminalAccessTargetName", + "", + ), + "start_terminal_access_target_label": getattr( + start_terminal_access_carrier, + "QetTerminalAccessTargetLabel", + "", + ), + "end_terminal_access_target_label": getattr( + end_terminal_access_carrier, + "QetTerminalAccessTargetLabel", + "", + ), + "start_terminal_access_target_rule": getattr( + start_terminal_access_carrier, + "QetTerminalAccessTargetRule", + "", + ), + "end_terminal_access_target_rule": getattr( + end_terminal_access_carrier, + "QetTerminalAccessTargetRule", + "", + ), + "start_terminal_access_fallback_target": str( + getattr(start_terminal_access_carrier, "QetTerminalAccessFallbackTarget", "") or "" + ) + == "1", + "end_terminal_access_fallback_target": str( + getattr(end_terminal_access_carrier, "QetTerminalAccessFallbackTarget", "") or "" + ) + == "1", + "start_terminal_access_avoided_endpoint_device": str( + getattr(start_terminal_access_carrier, "QetTerminalAccessAvoidedEndpointDevice", "") or "" + ) + == "1", + "end_terminal_access_avoided_endpoint_device": str( + getattr(end_terminal_access_carrier, "QetTerminalAccessAvoidedEndpointDevice", "") or "" + ) + == "1", + "start_terminal_access_target_distance": float( + getattr(start_terminal_access_carrier, "QetTerminalAccessTargetDistanceMm", 0.0) or 0.0 + ), + "end_terminal_access_target_distance": float( + getattr(end_terminal_access_carrier, "QetTerminalAccessTargetDistanceMm", 0.0) or 0.0 + ), + "start_terminal_access_target_component_primary_segments": int( + getattr( + start_terminal_access_carrier, + "QetTerminalAccessTargetComponentPrimarySegments", + 0, + ) + or 0 + ), + "end_terminal_access_target_component_primary_segments": int( + getattr( + end_terminal_access_carrier, + "QetTerminalAccessTargetComponentPrimarySegments", + 0, + ) + or 0 + ), "obstacle_aware": bool(obstacle_aware), "boundary_aware": bool(candidate_boundaries), "route_constraints": _route_constraint_payload(constraint_options), @@ -2313,6 +2704,28 @@ def _object_parent_chain(obj, limit=16): return chain +def _terminal_route_endpoint_metadata(terminal): + payload = { + "terminal_name": str(getattr(terminal, "Name", "") or ""), + "terminal_label": str(getattr(terminal, "Label", "") or ""), + "parent_device_name": "", + "parent_device_label": "", + "parent_device_instance_id": "", + "parent_device_element_uuid": "", + } + for parent in _object_parent_chain(terminal): + instance_id = str(getattr(parent, "QetInstanceId", "") or "").strip() + element_uuid = str(getattr(parent, "QetElementUuid", "") or "").strip() + if not instance_id and not element_uuid: + continue + payload["parent_device_name"] = str(getattr(parent, "Name", "") or "") + payload["parent_device_label"] = str(getattr(parent, "Label", "") or "") + payload["parent_device_instance_id"] = instance_id + payload["parent_device_element_uuid"] = element_uuid + break + return payload + + def _has_pass_through_obstacle_semantics(obj): if obj is None: return False @@ -3308,11 +3721,21 @@ def _payload_device_index(payload): if not isinstance(device, dict): continue element_uuid = str(device.get("element_uuid", "") or "").strip() - instance_id = str(device.get("instance_id", "") or "").strip() + instance_id = ( + str(device.get("instance_id", "") or "").strip() + or str(device.get("device_instance_id", "") or "").strip() + ) if element_uuid and element_uuid not in index["by_element"]: index["by_element"][element_uuid] = device if instance_id and instance_id not in index["by_instance"]: index["by_instance"][instance_id] = device + # v2 快照按 3D 设备实例组织,2D element_uuid 在端子数组里。 + for terminal in list(device.get("terminals", []) or []): + if not isinstance(terminal, dict): + continue + terminal_element_uuid = str(terminal.get("element_uuid", "") or "").strip() + if terminal_element_uuid and terminal_element_uuid not in index["by_element"]: + index["by_element"][terminal_element_uuid] = device return index @@ -3337,6 +3760,8 @@ def _payload_device_value(device, *names): return "" for name in names: value = device.get(name, "") + if (value is None or not str(value).strip()) and name == "instance_id": + value = device.get("device_instance_id", "") if value is not None and str(value).strip(): return str(value).strip() return "" @@ -4119,6 +4544,8 @@ def preflight_eplan_connections(doc, payload=None, options=None): opts.setdefault("__wire_style_cache", {}) project_uuid = str(source_payload.get("project_uuid", "") or _project_uuid(doc)).strip() terminals = index_terminals(doc) + terminal_candidates = _collect_routable_terminals(doc) + duplicate_terminal_summary = _terminal_uuid_duplicate_summary(terminal_candidates) local_terminal_count = sum( 1 for terminal_uuid in terminals @@ -4131,7 +4558,10 @@ def preflight_eplan_connections(doc, payload=None, options=None): "project_uuid": project_uuid, "total_wires": len(wires), "available_terminals": len(terminals), + "available_terminal_objects": len(terminal_candidates), "local_terminals": local_terminal_count, + "duplicate_terminal_uuid_count": duplicate_terminal_summary["duplicate_terminal_uuid_count"], + "duplicate_terminal_uuid_samples": duplicate_terminal_summary["duplicate_terminal_uuid_samples"], "route_network_carriers": 0, "route_network_segments": 0, "route_network_nodes": 0, @@ -4199,6 +4629,7 @@ def preflight_eplan_connections(doc, payload=None, options=None): path_diagnostic = RoutingNetwork.diagnose_routing_path_network( doc, terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), @@ -4232,8 +4663,10 @@ def preflight_eplan_connections(doc, payload=None, options=None): continue start_uuid = _wire_item_value(item, "start_terminal_uuid") end_uuid = _wire_item_value(item, "end_terminal_uuid") - start_found = bool(start_uuid and start_uuid in terminals) - end_found = bool(end_uuid and end_uuid in terminals) + start_match = _terminal_endpoint_match(terminal_candidates, item, "start") + end_match = _terminal_endpoint_match(terminal_candidates, item, "end") + start_found = start_match.get("terminal") is not None + end_found = end_match.get("terminal") is not None if start_found and end_found: continue for terminal_uuid, found in ((start_uuid, start_found), (end_uuid, end_found)): @@ -4250,16 +4683,24 @@ def preflight_eplan_connections(doc, payload=None, options=None): "start_element_uuid": _wire_item_value(item, "start_element_uuid"), "start_instance_id": _wire_item_value(item, "start_instance_id"), "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_terminal_candidate_count": int(start_match.get("candidate_count", 0) or 0), + "start_terminal_context_match_count": int(start_match.get("context_match_count", 0) or 0), + "start_terminal_match_reason_code": str(start_match.get("reason_code", "") or ""), + "start_terminal_match_ambiguous": bool(start_match.get("ambiguous", False)), "end_terminal_uuid": end_uuid, "end_found": end_found, "end_element_uuid": _wire_item_value(item, "end_element_uuid"), "end_instance_id": _wire_item_value(item, "end_instance_id"), "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_terminal_candidate_count": int(end_match.get("candidate_count", 0) or 0), + "end_terminal_context_match_count": int(end_match.get("context_match_count", 0) or 0), + "end_terminal_match_reason_code": str(end_match.get("reason_code", "") or ""), + "end_terminal_match_ambiguous": bool(end_match.get("ambiguous", False)), } if not start_found: - _add_missing_endpoint_terminal_context(sample, "start", terminals, doc=doc) + _add_missing_endpoint_terminal_context(sample, "start", terminal_candidates, doc=doc) if not end_found: - _add_missing_endpoint_terminal_context(sample, "end", terminals, doc=doc) + _add_missing_endpoint_terminal_context(sample, "end", terminal_candidates, doc=doc) report["missing_endpoint_samples"].append(sample) report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) @@ -4551,10 +4992,14 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la opts.setdefault("__wire_style_cache", {}) terminal_binding_report = bind_wire_task_terminals_from_payload(doc, payload) terminals = index_terminals(doc) + terminal_candidates = _collect_routable_terminals(doc) + duplicate_terminal_summary = _terminal_uuid_duplicate_summary(terminal_candidates) local_terminal_count = sum( 1 - for terminal_uuid in terminals - if TerminalObjects.is_local_terminal_uuid(terminal_uuid) + for terminal in terminal_candidates + if TerminalObjects.is_local_terminal_uuid( + _terminal_endpoint_value(terminal, "QetTerminalUuid") + ) ) wires = payload.get("wires", []) or [] payload_devices = _payload_device_index(payload) @@ -4564,7 +5009,10 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "runtime_version": AUTO_ROUTING_RUNTIME_VERSION, "total_wires": len(wires), "available_terminals": len(terminals), + "available_terminal_objects": len(terminal_candidates), "local_terminals": local_terminal_count, + "duplicate_terminal_uuid_count": duplicate_terminal_summary["duplicate_terminal_uuid_count"], + "duplicate_terminal_uuid_samples": duplicate_terminal_summary["duplicate_terminal_uuid_samples"], "auto_bound_terminals": terminal_binding_report["bound"], "auto_created_terminals": terminal_binding_report["created"], "auto_terminal_binding_warnings": terminal_binding_report["warnings"], @@ -4790,14 +5238,16 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la continue start_uuid = _wire_item_value(item, "start_terminal_uuid") end_uuid = _wire_item_value(item, "end_terminal_uuid") - start_terminal = terminals.get(start_uuid) - end_terminal = terminals.get(end_uuid) + start_match = _terminal_endpoint_match(terminal_candidates, item, "start") + end_match = _terminal_endpoint_match(terminal_candidates, item, "end") + start_terminal = start_match.get("terminal") + end_terminal = end_match.get("terminal") if start_terminal is None or end_terminal is None: report["skipped_missing_terminal"] += 1 add_status("MissingTerminal") set_item_task_status(item, "MissingTerminal") - for terminal_uuid in (start_uuid, end_uuid): - if terminal_uuid and terminal_uuid not in terminals: + for terminal_uuid, terminal in ((start_uuid, start_terminal), (end_uuid, end_terminal)): + if terminal_uuid and terminal is None: missing_endpoint_uuids.add(terminal_uuid) # 这里只保留少量样例,避免面板状态被大量导线任务刷屏。 if len(report["missing_endpoint_samples"]) < 8: @@ -4816,6 +5266,10 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "start_device_label": _wire_item_value(item, "start_device_label", "start_display_tag") or _payload_device_value(start_payload_device, "display_tag", "label", "name"), "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_terminal_candidate_count": int(start_match.get("candidate_count", 0) or 0), + "start_terminal_context_match_count": int(start_match.get("context_match_count", 0) or 0), + "start_terminal_match_reason_code": str(start_match.get("reason_code", "") or ""), + "start_terminal_match_ambiguous": bool(start_match.get("ambiguous", False)), "end_terminal_uuid": end_uuid, "end_found": end_terminal is not None, "end_element_uuid": _wire_item_value(item, "end_element_uuid"), @@ -4824,11 +5278,15 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "end_device_label": _wire_item_value(item, "end_device_label", "end_display_tag") or _payload_device_value(end_payload_device, "display_tag", "label", "name"), "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_terminal_candidate_count": int(end_match.get("candidate_count", 0) or 0), + "end_terminal_context_match_count": int(end_match.get("context_match_count", 0) or 0), + "end_terminal_match_reason_code": str(end_match.get("reason_code", "") or ""), + "end_terminal_match_ambiguous": bool(end_match.get("ambiguous", False)), } if start_terminal is None: - _add_missing_endpoint_terminal_context(sample, "start", terminals, doc=doc) + _add_missing_endpoint_terminal_context(sample, "start", terminal_candidates, doc=doc) if end_terminal is None: - _add_missing_endpoint_terminal_context(sample, "end", terminals, doc=doc) + _add_missing_endpoint_terminal_context(sample, "end", terminal_candidates, doc=doc) report["missing_endpoint_samples"].append(sample) continue if not has_route_network: @@ -5060,6 +5518,8 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la report["routed"] += 1 route_length = float(result.get("length_mm", 0.0) or 0.0) report["total_length_mm"] += route_length + start_endpoint_route_metadata = _terminal_route_endpoint_metadata(start_terminal) + end_endpoint_route_metadata = _terminal_route_endpoint_metadata(end_terminal) route_record = { "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), @@ -5067,13 +5527,37 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "wire_style_id": _wire_item_value(item, "wire_style_id"), "wire_style_status": result.get("wire_style_status", ""), "start_terminal_uuid": start_uuid, + "start_terminal_name": start_endpoint_route_metadata.get("terminal_name", ""), + "start_terminal_label": start_endpoint_route_metadata.get("terminal_label", ""), "start_element_uuid": _wire_item_value(item, "start_element_uuid"), "start_terminal_display": _wire_item_value(item, "start_terminal_display"), "start_device_label": endpoint_metadata.get("start_device_label", ""), + "start_parent_device_name": start_endpoint_route_metadata.get("parent_device_name", ""), + "start_parent_device_label": start_endpoint_route_metadata.get("parent_device_label", ""), + "start_parent_device_instance_id": start_endpoint_route_metadata.get( + "parent_device_instance_id", + "", + ), + "start_parent_device_element_uuid": start_endpoint_route_metadata.get( + "parent_device_element_uuid", + "", + ), "end_terminal_uuid": end_uuid, + "end_terminal_name": end_endpoint_route_metadata.get("terminal_name", ""), + "end_terminal_label": end_endpoint_route_metadata.get("terminal_label", ""), "end_element_uuid": _wire_item_value(item, "end_element_uuid"), "end_terminal_display": _wire_item_value(item, "end_terminal_display"), "end_device_label": endpoint_metadata.get("end_device_label", ""), + "end_parent_device_name": end_endpoint_route_metadata.get("parent_device_name", ""), + "end_parent_device_label": end_endpoint_route_metadata.get("parent_device_label", ""), + "end_parent_device_instance_id": end_endpoint_route_metadata.get( + "parent_device_instance_id", + "", + ), + "end_parent_device_element_uuid": end_endpoint_route_metadata.get( + "parent_device_element_uuid", + "", + ), "endpoint_label": _wire_item_value(item, "endpoint_label"), "algorithm": result["algorithm"], "route_status": result["route_status"], @@ -5337,6 +5821,30 @@ def _route_warning_carrier_labels(route_track, warning_kinds, limit=4): return labels +def _fallback_route_source_label_counts(report, limit=6): + counts = {} + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + route_track = route.get("route_track", {}) + quality = _route_quality_payload(route_track) + if quality.get("quality_status") != "FallbackPathWarning": + continue + labels = _route_warning_carrier_labels( + route_track, + quality.get("fallback_carrier_kinds", []), + limit=8, + ) + if not labels: + labels = ["未命名布线面/辅助路径"] + for label in labels: + counts[label] = counts.get(label, 0) + 1 + return { + key: counts[key] + for key in sorted(counts, key=lambda item: (-counts[item], item))[: int(limit or 0)] + } + + def _route_network_metric_max(report, key): maximum = 0 for route in report.get("routes", []) or []: @@ -5416,6 +5924,72 @@ def _route_track_min_capacity(route_track): return min(capacities) +def _route_track_bottleneck_carriers(route_track, min_capacity=None, limit=8): + if not isinstance(route_track, dict): + return { + "names": [], + "kinds": [], + "source_labels": [], + } + try: + target_capacity = int(float(min_capacity)) if min_capacity is not None else _route_track_min_capacity(route_track) + except Exception: + target_capacity = _route_track_min_capacity(route_track) + if target_capacity is None or target_capacity <= 0: + return { + "names": [], + "kinds": [], + "source_labels": [], + } + max_items = int(limit or 0) + names = [] + kinds = [] + source_labels = [] + seen_names = set() + seen_kinds = set() + seen_sources = set() + for segment in route_track.get("segments", []) or []: + # 只看真实路径段;虚拟桥接段不代表实际线槽/路径容量。 + if isinstance(segment, dict) and bool(segment.get("is_bridge", False)): + continue + carrier = segment.get("carrier", {}) if isinstance(segment, dict) else {} + if not isinstance(carrier, dict): + continue + try: + capacity = int(float(carrier.get("capacity", 0) or 0)) + except Exception: + capacity = 0 + if capacity != target_capacity: + continue + name = str(carrier.get("name", "") or "").strip() + if name and name not in seen_names: + seen_names.add(name) + names.append(name) + kind = str(carrier.get("kind", "") or "").strip() + if kind and kind not in seen_kinds: + seen_kinds.add(kind) + kinds.append(kind) + label = ( + str(carrier.get("source_label", "") or "").strip() + or str(carrier.get("source_name", "") or "").strip() + or str(carrier.get("label", "") or "").strip() + or name + ) + source_path_index = str(carrier.get("source_path_index", "") or "").strip() + if label and source_path_index: + label = "{0}(路径{1})".format(label, source_path_index) + if label and label not in seen_sources: + seen_sources.add(label) + source_labels.append(label) + if max_items > 0 and len(names) >= max_items and len(source_labels) >= max_items: + break + return { + "names": names[:max_items] if max_items > 0 else names, + "kinds": kinds[:max_items] if max_items > 0 else kinds, + "source_labels": source_labels[:max_items] if max_items > 0 else source_labels, + } + + def _route_capacity_pressure_summary(report): samples = _route_capacity_pressure_samples(report, limit=0) if not samples: @@ -5450,6 +6024,11 @@ def _route_capacity_pressure_samples(report, limit=8): continue if max_samples <= 0 or len(samples) < max_samples: route_track = route.get("route_track", {}) + bottlenecks = _route_track_bottleneck_carriers( + route_track, + min_capacity=route_capacity, + limit=4, + ) samples.append( { "wire_uuid": route.get("wire_uuid", ""), @@ -5463,6 +6042,9 @@ def _route_capacity_pressure_samples(report, limit=8): "lane_index": int(lane.get("index", 0) or 0), "carrier_names": _route_track_carrier_names(route_track, limit=4), "route_source_labels": _route_source_labels(route_track, limit=4), + "bottleneck_carrier_names": bottlenecks["names"], + "bottleneck_carrier_kinds": bottlenecks["kinds"], + "bottleneck_route_source_labels": bottlenecks["source_labels"], } ) return samples @@ -5554,6 +6136,164 @@ def _route_path_usage_summary(report): return summary +def _terminal_access_usage_summary(report): + summary = { + "routes": 0, + "both_endpoints_consumed": 0, + "one_endpoint_consumed": 0, + "no_endpoint_consumed": 0, + "start_consumed": 0, + "end_consumed": 0, + } + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + start_consumed = bool(network.get("start_terminal_access_consumed", False)) + end_consumed = bool(network.get("end_terminal_access_consumed", False)) + summary["routes"] += 1 + if start_consumed: + summary["start_consumed"] += 1 + if end_consumed: + summary["end_consumed"] += 1 + if start_consumed and end_consumed: + summary["both_endpoints_consumed"] += 1 + elif start_consumed or end_consumed: + summary["one_endpoint_consumed"] += 1 + else: + summary["no_endpoint_consumed"] += 1 + return summary + + +def _terminal_access_target_kind_counts(report): + counts = {} + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + for key in ("start_terminal_access_target_kind", "end_terminal_access_target_kind"): + kind = str(network.get(key, "") or "").strip() + if not kind: + continue + counts[kind] = counts.get(kind, 0) + 1 + preferred_order = { + "WireDuct": 0, + "WireDuctOpenEnd": 1, + "UserPath": 2, + "WiringCutOut": 3, + "RoutingPath": 4, + "RoutingRange": 10, + "AuxiliaryPath": 11, + } + return { + key: counts[key] + for key in sorted(counts, key=lambda item: (preferred_order.get(item, 100), -counts[item], item)) + } + + +def _terminal_access_fallback_target_count(report): + counts = _terminal_access_target_kind_counts(report) + return sum(_safe_int(counts.get(kind, 0)) for kind in ("RoutingRange", "AuxiliaryPath")) + + +def _terminal_access_fallback_target_samples(report, limit=8): + samples = [] + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + for endpoint in ("start", "end"): + kind = str(network.get("{0}_terminal_access_target_kind".format(endpoint), "") or "").strip() + if kind not in {"RoutingRange", "AuxiliaryPath"}: + continue + sample = { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "endpoint": endpoint, + "target_kind": kind, + "target_name": str(network.get("{0}_terminal_access_target_name".format(endpoint), "") or ""), + "target_label": str(network.get("{0}_terminal_access_target_label".format(endpoint), "") or ""), + "target_distance": float( + network.get("{0}_terminal_access_target_distance".format(endpoint), 0.0) + or 0.0 + ), + } + optional_fields = { + "terminal_uuid": str(route.get("{0}_terminal_uuid".format(endpoint), "") or ""), + "terminal_name": str(route.get("{0}_terminal_name".format(endpoint), "") or ""), + "terminal_label": str( + route.get("{0}_terminal_label".format(endpoint), "") + or route.get("{0}_terminal_display".format(endpoint), "") + or "" + ), + "parent_device_name": str( + route.get("{0}_parent_device_name".format(endpoint), "") + or route.get("{0}_device_name".format(endpoint), "") + or "" + ), + "parent_device_label": str( + route.get("{0}_parent_device_label".format(endpoint), "") + or route.get("{0}_device_label".format(endpoint), "") + or "" + ), + "parent_device_instance_id": str( + route.get("{0}_parent_device_instance_id".format(endpoint), "") + or route.get("{0}_instance_id".format(endpoint), "") + or "" + ), + "parent_device_element_uuid": str( + route.get("{0}_parent_device_element_uuid".format(endpoint), "") + or route.get("{0}_element_uuid".format(endpoint), "") + or "" + ), + } + sample.update({key: value for key, value in optional_fields.items() if value}) + samples.append(sample) + if int(limit or 0) <= 0: + return samples + return samples[: int(limit or 0)] + + +def _terminal_access_fallback_targets(report): + if not isinstance(report, dict): + return False + if _route_network_main_path_carriers(report) <= 0: + return False + return _terminal_access_fallback_target_count(report) > 0 + + +def _terminal_access_fallback_targets_text(report): + if not _terminal_access_fallback_targets(report): + return "" + text = ( + "端子接入退回布线面:当前有线槽/UserPath/过线孔主路径 {0} 条," + "但仍有 {1} 个端子接入目标为 RoutingRange/辅助路径。" + ).format( + _route_network_main_path_carriers(report), + _terminal_access_fallback_target_count(report), + ) + samples = _terminal_access_fallback_target_samples(report, limit=1) + if samples: + sample = samples[0] + endpoint_text = "起点" if sample.get("endpoint") == "start" else "终点" + wire_text = str(sample.get("wire_label", "") or sample.get("wire_uuid", "") or "未知导线") + target_text = str(sample.get("target_label", "") or sample.get("target_kind", "") or "布线面") + text += "示例 {0} {1}接入到 {2},距离 {3:.1f}mm。".format( + wire_text, + endpoint_text, + target_text, + float(sample.get("target_distance", 0.0) or 0.0), + ) + text += "请检查端子附近是否缺少 UserPath/线槽桥接,或主路径是否被孤立。" + return text + + def _route_network_carrier_kind_counts(network): counts = {} if not isinstance(network, dict): @@ -5657,6 +6397,41 @@ def _main_path_not_used_text(report): ).format(fallback_routes) +def _main_path_underused(report): + if not isinstance(report, dict): + return False + if _safe_int(report.get("routed", 0)) <= 0: + return False + if _route_network_main_path_carriers(report) <= 0: + return False + usage = _route_path_usage_summary(report) + main_path_routes = _safe_int(usage.get("main_path_routes", 0)) + fallback_routes = _safe_int(usage.get("fallback_routes", 0)) + if main_path_routes <= 0 or fallback_routes <= 0: + return False + return fallback_routes >= main_path_routes * 2 + + +def _main_path_underused_text(report): + if not _main_path_underused(report): + return "" + usage = _route_path_usage_summary(report) + main_path_routes = _safe_int(usage.get("main_path_routes", 0)) + fallback_routes = _safe_int(usage.get("fallback_routes", 0)) + routed = max(1, _safe_int(report.get("routed", main_path_routes + fallback_routes))) + main_path_carriers = _route_network_main_path_carriers(report) + text = ( + "主路径使用率过低:当前有线槽/UserPath/过线孔路径 {0} 条," + "本批次 {1} 条导线中只有 {2} 条使用主路径,{3} 条仍走布线面/辅助路径。" + ).format(main_path_carriers, routed, main_path_routes, fallback_routes) + fallback_counts = _fallback_route_source_label_counts(report, limit=3) + if fallback_counts: + fallback_text = ",".join("{0} {1} 条".format(label, count) for label, count in fallback_counts.items()) + text += " 主要兜底路径:{0}。".format(fallback_text) + text += " 建议补 UserPath/线槽到端子区域的桥接,或标记柜内边界,避免整体退回柜板布线面。" + return text + + def _has_routing_path_network_diagnostic(report): if not isinstance(report, dict): return False @@ -5689,6 +6464,7 @@ def _attach_routing_path_network_diagnostic_if_needed(doc, report, opts): RoutingNetwork.diagnose_routing_path_network( doc, terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), @@ -6616,6 +7392,26 @@ def format_eplan_connection_route_report(report): if created_count > 0: reroute_text = "并重跑布线" if bool(auto_detour_bridges.get("rerouted", False)) else "" message += "\n自动主路径补桥:生成 UserPath {0} 条{1}。".format(created_count, reroute_text) + pair_labels = [ + str(label or "").strip() + for label in list(auto_detour_bridges.get("created_pair_labels", []) or []) + if str(label or "").strip() + ] + if pair_labels: + message += " 配对:{0}。".format("、".join(pair_labels[:3])) + auto_terminal_bridges = report.get("auto_terminal_access_fallback_bridges", {}) + if isinstance(auto_terminal_bridges, dict): + created_count = _safe_int(auto_terminal_bridges.get("created_count", 0)) + if created_count > 0: + reroute_text = "并重跑布线" if bool(auto_terminal_bridges.get("rerouted", False)) else "" + message += "\n自动端子接入补桥:生成 UserPath {0} 条{1}。".format(created_count, reroute_text) + pair_labels = [ + str(label or "").strip() + for label in list(auto_terminal_bridges.get("created_pair_labels", []) or []) + if str(label or "").strip() + ] + if pair_labels: + message += " 配对:{0}。".format("、".join(pair_labels[:3])) path_diagnostic = report.get("routing_path_network_diagnostic", {}) if isinstance(path_diagnostic, dict) and int(path_diagnostic.get("issue_count", 0) or 0) > 0: issue_labels = [ @@ -6714,6 +7510,11 @@ def format_eplan_connection_route_report(report): message += " 示例导线 {0}".format(sample.get("wire")) route_labels = list(sample.get("route_source_labels", []) or []) carrier_names = list(sample.get("carrier_names", []) or []) + bottleneck_labels = list(sample.get("bottleneck_route_source_labels", []) or []) + bottleneck_names = list(sample.get("bottleneck_carrier_names", []) or []) + bottleneck_paths = bottleneck_labels or bottleneck_names + if bottleneck_paths: + message += ",瓶颈路径 {0}".format("、".join(bottleneck_paths)) path_labels = route_labels or carrier_names if path_labels: message += ",路径 {0}".format("、".join(path_labels)) @@ -6814,6 +7615,12 @@ def format_eplan_connection_route_report(report): route_labels = list(sample.get("route_source_labels", []) or []) if route_labels: route_text = ",路径 {0}".format("、".join(route_labels)) + message += "\n导线越出柜内区域:{0} 条,示例导线 {1} {2} 个越界点{3}。".format( + boundary_warning.get("count", 0), + sample.get("wire", "未知导线"), + sample.get("violations", 0), + route_text, + ) message += "\n柜内边界提示:{0} 条导线最终路径仍越出柜内区域,示例导线 {1} {2} 个越界点{3}。请补柜内 UserPath/线槽或调整柜内边界。".format( boundary_warning.get("count", 0), sample.get("wire", "未知导线"), @@ -6847,9 +7654,29 @@ def format_eplan_connection_route_report(report): main_path_routes, fallback_routes, ) + terminal_access_usage = _terminal_access_usage_summary(report) + if _safe_int(terminal_access_usage.get("routes", 0)) > 0: + message += "\n端子接入采用:两端接入 {0} 条,一端接入 {1} 条,未接入 {2} 条。".format( + _safe_int(terminal_access_usage.get("both_endpoints_consumed", 0)), + _safe_int(terminal_access_usage.get("one_endpoint_consumed", 0)), + _safe_int(terminal_access_usage.get("no_endpoint_consumed", 0)), + ) + terminal_access_target_counts = _terminal_access_target_kind_counts(report) + if terminal_access_target_counts: + target_text = ",".join( + "{0} {1} 个".format(kind, count) + for kind, count in terminal_access_target_counts.items() + ) + message += "\n端子接入目标:{0}。".format(target_text) + fallback_target_text = _terminal_access_fallback_targets_text(report) + if fallback_target_text: + message += "\n{0}".format(fallback_target_text) main_path_text = _main_path_not_used_text(report) if main_path_text: message += "\n{0}".format(main_path_text) + main_path_underused_text = _main_path_underused_text(report) + if main_path_underused_text: + message += "\n{0}".format(main_path_underused_text) quality_warning = _route_quality_warning_summary(report) if quality_warning: message += "\n路径质量提示:{0} 条导线使用布线面/辅助路径,可能没有完全优先进入线槽。".format( @@ -7122,6 +7949,9 @@ _ROUTING_DIAGNOSTIC_ISSUE_LABELS = { "invalid_route_carriers": "路径对象几何无效", "routing_range_only_network": "仅使用布线面兜底", "main_path_not_used": "未使用线槽或用户主路径", + "main_path_underused": "主路径使用率过低", + "terminal_access_fallback_targets": "端子接入退回布线面", + "invalid_terminal_exit_directions": "端子出线方向无效", "invalid_terminal_local_routes": "端子局部路径无效", "route_carriers_outside_boundary": "路径越出柜内边界", "terminals_outside_boundary": "端子越出柜内边界", @@ -7132,6 +7962,8 @@ _ROUTING_DIAGNOSTIC_ISSUE_LABELS = { "isolated_network_components": "存在孤立路径网络", "routing_errors": "布线计算错误", "collision_warnings": "碰撞告警", + "hard_intersections": "硬穿模", + "clearance_warnings": "间隙不足", "structural_collision_candidates": "结构件碰撞候选", "device_or_layout_collisions": "设备/布局碰撞", "third_party_device_collisions": "第三方设备/布局碰撞", @@ -7572,6 +8404,7 @@ def _routing_diagnostic_recommended_actions(summary): _safe_int(issue_counts.get("route_candidate_boundary_violations", 0)) > 0 or _safe_int(issue_counts.get("boundary_warning", 0)) > 0 ): + add("点击“选择越界导线”定位越出柜内区域的导线及其路径") add("检查柜内边界和 UserPath,必要时补柜内主路径") if isinstance(issue_counts, dict) and _safe_int(issue_counts.get("route_capacity_pressure", 0)) > 0: add("检查路径容量,必要时补备用路径或提高线槽容量") @@ -7579,6 +8412,8 @@ def _routing_diagnostic_recommended_actions(summary): add("点击“选择缺主路径导线”定位需要补 UserPath 或主路径桥接的导线") issue_codes = set(str(code or "").strip() for code in list(summary.get("issue_codes", []) or [])) + hard_collision_count = _safe_int(issue_counts.get("hard_intersections", 0)) if isinstance(issue_counts, dict) else 0 + clearance_collision_count = _safe_int(issue_counts.get("clearance_warnings", 0)) if isinstance(issue_counts, dict) else 0 if "main_path_detour_missing" in issue_codes: add("点击“选择缺主路径导线”定位需要补 UserPath 或主路径桥接的导线") add("点击“选择缺主路径线路径”对照当前实际路径") @@ -7586,6 +8421,35 @@ def _routing_diagnostic_recommended_actions(summary): if isinstance(detour_summary, dict) and list(detour_summary.get("rejected_fallback_labels", []) or []): add("点击“选择缺主路径补路位置”快速定位汇总需补区域") add("选中缺主路径导线后点击“选择拒绝兜底路径”查看需补路径位置") + if "terminal_access_fallback_targets" in issue_codes: + add("优先补端子附近到线槽/UserPath 的接入桥,避免端子接入退回布线面") + add("点击“按诊断建议生成桥接”尝试自动补端子退回目标到最近主路径的 UserPath 桥") + diagnostics = summary.get("diagnostics", {}) or {} + batch_payload = ((diagnostics.get("RoutingConnectionBatch", {}) or {}).get("payload", {}) or {}) + path_payload = ((diagnostics.get("RoutingPathNetwork", {}) or {}).get("payload", {}) or {}) + has_batch_samples = isinstance(batch_payload, dict) and list( + batch_payload.get("terminal_access_fallback_target_samples", []) or [] + ) + has_path_samples = isinstance(path_payload, dict) and list( + path_payload.get("terminal_access_fallback_targets", []) or [] + ) + if has_batch_samples or has_path_samples: + add("按端子接入退回布线面示例定位设备侧缺口,再重新生成布线路径网络") + if "unconnected_terminals" in issue_codes: + add("点击“选择未接入端子”定位未接入路由网络或接入距离超限的端子") + add("补端子附近 UserPath/线槽入口,或确认设备装配位置和端子接入最大距离") + if ( + "terminal_exit_direction_corrected" in issue_codes + or "terminal_exit_length_capped" in issue_codes + or "invalid_terminal_exit_directions" in issue_codes + or "invalid_terminal_local_routes" in issue_codes + ): + add("点击“选择出线问题端子”定位方向校正、长度截断、显式方向无效或局部路径无效的端子") + add("复查设备模板 CPoint/LCS 出线方向,必要时设置端子局部出线路径") + if "invalid_terminal_exit_directions" in issue_codes: + add("检查 QetTerminalExitDirectionJson,必要时用“选中端子设置出线方向”重写显式方向") + if "invalid_terminal_local_routes" in issue_codes: + add("检查 QetTerminalLocalRoutePointsJson,必要时用“选中端子设置局部出线”重写局部路径") collision_count = 0 batch_payload = ((summary.get("diagnostics", {}) or {}).get("RoutingConnectionBatch", {}) or {}).get("payload", {}) if isinstance(batch_payload, dict): @@ -7595,6 +8459,10 @@ def _routing_diagnostic_recommended_actions(summary): or collision_count > 0 or _safe_int(issue_counts.get("collision_warnings", 0)) > 0 ): + if "hard_intersections" in issue_codes or hard_collision_count > 0: + add("硬穿模:优先补 UserPath/线槽主路径或调整装配,不能直接忽略") + if "clearance_warnings" in issue_codes or clearance_collision_count > 0: + add("间隙不足:核对设备安全间隙、线槽/UserPath位置,必要时补路径或调整装配") collision_resolution = summary.get("batch_collision_resolution_summary", {}) if isinstance(collision_resolution, dict): counts = collision_resolution.get("counts", {}) @@ -7619,10 +8487,9 @@ def _routing_diagnostic_recommended_actions(summary): and (list(item.get("parent_names", []) or []) or list(item.get("parent_labels", []) or [])) for item in top_obstacles ) + add("点击“选择高发碰撞对象”和“选择碰撞导线”核对穿模位置") if has_parent_refs: add("点击“选择碰撞父装配”确认结构件后再标记忽略碰撞") - else: - add("点击“选择高发碰撞对象”和“选择碰撞导线”核对穿模位置") return actions @@ -8111,6 +8978,32 @@ def _compact_route_sample(route): ), "route_constraints": network.get("route_constraints", {}), } + sample["network"]["terminal_access"] = { + "start_consumed": bool(network.get("start_terminal_access_consumed", False)), + "end_consumed": bool(network.get("end_terminal_access_consumed", False)), + "start_carrier": str(network.get("start_terminal_access_carrier", "") or ""), + "end_carrier": str(network.get("end_terminal_access_carrier", "") or ""), + "start_label": str(network.get("start_terminal_access_label", "") or ""), + "end_label": str(network.get("end_terminal_access_label", "") or ""), + "start_target_kind": str(network.get("start_terminal_access_target_kind", "") or ""), + "end_target_kind": str(network.get("end_terminal_access_target_kind", "") or ""), + "start_target_name": str(network.get("start_terminal_access_target_name", "") or ""), + "end_target_name": str(network.get("end_terminal_access_target_name", "") or ""), + "start_target_label": str(network.get("start_terminal_access_target_label", "") or ""), + "end_target_label": str(network.get("end_terminal_access_target_label", "") or ""), + "start_target_distance": float( + network.get("start_terminal_access_target_distance", 0.0) or 0.0 + ), + "end_target_distance": float( + network.get("end_terminal_access_target_distance", 0.0) or 0.0 + ), + "start_target_component_primary_segments": int( + network.get("start_terminal_access_target_component_primary_segments", 0) or 0 + ), + "end_target_component_primary_segments": int( + network.get("end_terminal_access_target_component_primary_segments", 0) or 0 + ), + } return sample @@ -8187,6 +9080,8 @@ def _routing_connection_batch_issue_codes(report): bool(_route_quality_warning_samples(report, limit=1)), ), ("main_path_not_used", _main_path_not_used(report)), + ("main_path_underused", _main_path_underused(report)), + ("terminal_access_fallback_targets", _terminal_access_fallback_targets(report)), ( "long_terminal_access", bool(_long_network_entry_warning_samples(report, limit=1)), @@ -8567,6 +9462,116 @@ def _main_path_detour_wire_uuids_from_report(report): return wire_uuids +def _terminal_access_fallback_wire_uuids_from_report(report): + wire_uuids = [] + seen = set() + for sample in _terminal_access_fallback_target_samples(report, limit=0): + if not isinstance(sample, dict): + continue + wire_uuid = str(sample.get("wire_uuid", "") or "").strip() + if not wire_uuid or wire_uuid in seen: + continue + seen.add(wire_uuid) + wire_uuids.append(wire_uuid) + return wire_uuids + + +def _create_terminal_access_fallback_bridges_from_report(doc, report, project_uuid=""): + samples = [] + if isinstance(report, dict): + samples = [ + item + for item in list(report.get("terminal_access_fallback_target_samples", []) or []) + if isinstance(item, dict) + ] + if not samples: + samples = [ + item + for item in list(report.get("terminal_access_fallback_targets", []) or []) + if isinstance(item, dict) + ] + if not samples: + samples = _terminal_access_fallback_target_samples(report, limit=0) + main_path_kinds = {"WireDuct", "WireDuctOpenEnd", "UserPath", "WiringCutOut", "RoutingPath"} + main_candidates = [ + carrier + for carrier in RoutingNetwork.collect_route_carriers(doc) + if str(getattr(carrier, "QetRouteCarrierKind", "") or "").strip() in main_path_kinds + ] + created = [] + duplicates = 0 + missing_refs = [] + seen_targets = set() + + for sample in samples: + if not isinstance(sample, dict): + continue + target_kind = str(sample.get("target_kind", "") or "").strip() + if target_kind not in {"RoutingRange", "AuxiliaryPath"}: + continue + target_name = str(sample.get("target_name", "") or "").strip() + target_label = str(sample.get("target_label", "") or "").strip() + target_matches = _find_route_bridge_sources_by_name_or_label(doc, name=target_name, label="") + if not target_matches: + target_matches = _find_route_bridge_sources_by_name_or_label(doc, name=target_label, label=target_label) + if not target_matches: + missing_ref = target_name or target_label or target_kind + if missing_ref and missing_ref not in missing_refs: + missing_refs.append(missing_ref) + continue + target = target_matches[0] + target_ref = getattr(target, "Name", "") or target_name or target_label + if target_ref in seen_targets: + continue + seen_targets.add(target_ref) + + best_main = None + best_distance = None + for candidate in main_candidates: + if candidate is target: + continue + bridge_candidate = RoutingNetwork.nearest_route_bridge_candidate_between_objects( + doc, + target, + candidate, + ) + if not isinstance(bridge_candidate, dict): + continue + distance = float(bridge_candidate.get("distance_mm", 0.0) or 0.0) + if best_distance is None or distance < best_distance: + best_distance = distance + best_main = candidate + if best_main is None: + missing_ref = "{0} -> 主路径".format(target_label or target_name or target_kind) + if missing_ref not in missing_refs: + missing_refs.append(missing_ref) + continue + new_bridges = RoutingNetwork.create_user_path_bridge_between_objects( + doc, + target, + best_main, + project_uuid=project_uuid, + bridge_kind="TerminalAccessFallbackBridge", + ) + if new_bridges: + created.extend(new_bridges) + else: + duplicates += 1 + + return { + "enabled": True, + "targets": len(seen_targets), + "created_count": len(created), + "duplicates": duplicates, + "missing_targets": missing_refs, + "created_pair_labels": [ + getattr(bridge, "QetRouteBridgePairLabel", "") + for bridge in created + ], + "rerouted": False, + } + + def _wire_item_uuid(item): if not isinstance(item, dict): return "" @@ -8640,7 +9645,7 @@ def _recompute_route_report_after_route_replacement(doc, report): return report -def _merge_retry_routes_into_report(doc, report, retry_report): +def _merge_retry_routes_into_report(doc, report, retry_report, retry_prefix="main_path_detour"): if not isinstance(report, dict) or not isinstance(retry_report, dict): return report retry_routes = { @@ -8662,8 +9667,9 @@ def _merge_retry_routes_into_report(doc, report, retry_report): else: merged_routes.append(route) report["routes"] = merged_routes - report["main_path_detour_retry_wires"] = len(retry_routes) - report["main_path_detour_retry_replaced_routes"] = replaced + prefix = str(retry_prefix or "main_path_detour").strip() or "main_path_detour" + report["{0}_retry_wires".format(prefix)] = len(retry_routes) + report["{0}_retry_replaced_routes".format(prefix)] = replaced return _recompute_route_report_after_route_replacement(doc, report) @@ -8765,6 +9771,10 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): payload["auto_diagnostic_bridges"] = dict(report.get("auto_diagnostic_bridges") or {}) if isinstance(report.get("auto_main_path_detour_bridges"), dict): payload["auto_main_path_detour_bridges"] = dict(report.get("auto_main_path_detour_bridges") or {}) + if isinstance(report.get("auto_terminal_access_fallback_bridges"), dict): + payload["auto_terminal_access_fallback_bridges"] = dict( + report.get("auto_terminal_access_fallback_bridges") or {} + ) if isinstance(report.get("route_status_counts"), dict): payload["route_status_counts"] = dict(report.get("route_status_counts") or {}) carrier_kind_counts = _report_route_network_carrier_kind_counts(report) @@ -8823,6 +9833,20 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): payload["route_samples"] = [_compact_route_sample(route) for route in prioritized_routes[:limit]] payload["route_sample_count"] = len(payload["route_samples"]) payload["route_path_usage"] = _route_path_usage_summary(report) + payload["terminal_access_usage"] = _terminal_access_usage_summary(report) + payload["terminal_access_target_kind_counts"] = _terminal_access_target_kind_counts(report) + payload["terminal_access_fallback_target_count"] = len( + _terminal_access_fallback_target_samples(report, limit=0) + ) + payload["terminal_access_fallback_target_samples"] = _terminal_access_fallback_target_samples( + report, + limit=limit, + ) + payload["main_path_usage"] = { + "main_path_carriers": _route_network_main_path_carriers(report), + "underused": _main_path_underused(report), + "fallback_source_label_counts": _fallback_route_source_label_counts(report, limit=limit), + } route_quality_warnings = _route_quality_warning_samples(report, limit=limit) payload["route_quality_warning_count"] = len(_route_quality_warning_samples(report, limit=0)) payload["route_quality_warning_samples"] = route_quality_warnings @@ -8841,6 +9865,9 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): _route_candidate_boundary_warning_samples(report, limit=0) ) payload["route_candidate_boundary_warning_samples"] = candidate_boundary_warnings + # 给界面/诊断 JSON 一个更工程化的别名,避免用户只看到算法内部的 candidate 命名。 + payload["wire_outside_boundary_count"] = payload["route_candidate_boundary_warning_count"] + payload["wire_outside_boundary_samples"] = candidate_boundary_warnings route_constraint_samples = _route_constraint_samples(report, limit=limit) payload["route_constraint_warning_count"] = len(_route_constraint_samples(report, limit=0)) payload["route_constraint_warning_samples"] = route_constraint_samples @@ -8950,6 +9977,7 @@ def _direct_task_routing_path_network_diagnostic(doc, opts): RoutingNetwork.diagnose_routing_path_network( doc, terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), @@ -9076,6 +10104,7 @@ def generate_eplan_routing_path_network(doc, project_uuid="", options=None, sele project_uuid=target_project_uuid, selection_ex=selection_ex, terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), ) @@ -9096,6 +10125,7 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None): doc, project_uuid=target_project_uuid, terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), @@ -9174,6 +10204,24 @@ def _compact_routing_path_network_diagnostic(diagnostic): } for item in long_accesses[:5] ] + capped_exits = _dict_items(diagnostic.get("capped_terminal_exits", []) or []) + if capped_exits: + payload["capped_terminal_exits"] = [ + _compact_terminal_exit_diagnostic_sample(item) + for item in capped_exits[:8] + ] + corrected_exits = _dict_items(diagnostic.get("corrected_terminal_exits", []) or []) + if corrected_exits: + payload["corrected_terminal_exits"] = [ + _compact_terminal_exit_diagnostic_sample(item) + for item in corrected_exits[:8] + ] + invalid_exit_directions = _dict_items(diagnostic.get("invalid_terminal_exit_directions", []) or []) + if invalid_exit_directions: + payload["invalid_terminal_exit_directions"] = [ + _compact_terminal_metadata_issue_sample(item) + for item in invalid_exit_directions[:8] + ] wire_duct_components = _dict_items(diagnostic.get("wire_ducts_without_terminal_access", []) or []) if wire_duct_components: payload["wire_ducts_without_terminal_access"] = [ @@ -9189,9 +10237,85 @@ def _compact_routing_path_network_diagnostic(diagnostic): } for item in wire_duct_components[:5] ] + terminal_access_fallbacks = _dict_items(diagnostic.get("terminal_access_fallback_targets", []) or []) + if terminal_access_fallbacks: + payload["terminal_access_fallback_targets"] = [ + _compact_terminal_access_quality_sample(item) + for item in terminal_access_fallbacks[:8] + ] + terminal_access_endpoint_avoidance = _dict_items( + diagnostic.get("terminal_access_endpoint_device_avoidance", []) or [] + ) + if terminal_access_endpoint_avoidance: + payload["terminal_access_endpoint_device_avoidance"] = [ + _compact_terminal_access_quality_sample(item) + for item in terminal_access_endpoint_avoidance[:8] + ] return payload +def _compact_terminal_access_quality_sample(item): + return { + "access_carrier_name": item.get("access_carrier_name", ""), + "access_carrier_label": item.get("access_carrier_label", ""), + "terminal_name": item.get("terminal_name", ""), + "terminal_label": item.get("terminal_label", ""), + "terminal_uuid": item.get("terminal_uuid", ""), + "instance_id": item.get("instance_id", ""), + "parent_device_name": item.get("parent_device_name", ""), + "parent_device_label": item.get("parent_device_label", ""), + "parent_device_instance_id": item.get("parent_device_instance_id", ""), + "parent_device_element_uuid": item.get("parent_device_element_uuid", ""), + "target_kind": item.get("target_kind", ""), + "target_name": item.get("target_name", ""), + "target_label": item.get("target_label", ""), + "target_rule": item.get("target_rule", ""), + "target_distance_mm": item.get("target_distance_mm", 0.0), + } + + +def _compact_terminal_metadata_issue_sample(item): + return { + "name": item.get("name", ""), + "label": item.get("label", ""), + "terminal_uuid": item.get("terminal_uuid", ""), + "instance_id": item.get("instance_id", ""), + "parent_device_name": item.get("parent_device_name", ""), + "parent_device_label": item.get("parent_device_label", ""), + "parent_device_instance_id": item.get("parent_device_instance_id", ""), + "parent_device_element_uuid": item.get("parent_device_element_uuid", ""), + "property_name": item.get("property_name", ""), + "reason": item.get("reason", ""), + "message": item.get("message", ""), + "raw_sample": item.get("raw_sample", ""), + } + + +def _compact_terminal_exit_diagnostic_sample(item): + return { + "name": item.get("name", ""), + "label": item.get("label", ""), + "terminal_uuid": item.get("terminal_uuid", ""), + "instance_id": item.get("instance_id", ""), + "parent_device_name": item.get("parent_device_name", ""), + "parent_device_label": item.get("parent_device_label", ""), + "parent_device_instance_id": item.get("parent_device_instance_id", ""), + "parent_device_element_uuid": item.get("parent_device_element_uuid", ""), + "exit_rule": item.get("exit_rule", ""), + "exit_direction_source": item.get("exit_direction_source", ""), + "exit_direction": item.get("exit_direction", {}), + "original_exit_direction": item.get("original_exit_direction", {}), + "exit_direction_corrected": bool(item.get("exit_direction_corrected", False)), + "requested_exit_length_mm": item.get("requested_exit_length_mm", 0.0), + "actual_exit_length_mm": item.get("actual_exit_length_mm", 0.0), + "max_exit_length_mm": item.get("max_exit_length_mm", 0.0), + "device_exit_required_length_mm": item.get("device_exit_required_length_mm", 0.0), + "original_device_exit_required_length_mm": item.get("original_device_exit_required_length_mm", 0.0), + "exit_length_capped": bool(item.get("exit_length_capped", False)), + "device_bbox_detected": bool(item.get("device_bbox_detected", False)), + } + + _PATH_NETWORK_ISSUE_LABELS = { "empty_routing_path_network": "布线路径网络为空", "invalid_route_carriers": "路径对象几何无效", @@ -9200,6 +10324,10 @@ _PATH_NETWORK_ISSUE_LABELS = { "route_carriers_outside_boundary": "路径越出柜内边界", "terminals_outside_boundary": "端子越出柜内边界", "long_terminal_accesses": "端子接入过长", + "terminal_exit_length_capped": "端子出线长度截断", + "terminal_exit_direction_corrected": "端子默认出线方向校正", + "terminal_access_fallback_targets": "端子接入退回布线面", + "terminal_access_endpoint_device_avoidance": "端子接入避让端点设备", "unconnected_terminals": "端子未接入", "wire_duct_endpoint_breaks": "线槽端点疑似断开", "wire_ducts_without_terminal_access": "线槽未接入端子主网络", @@ -9353,6 +10481,14 @@ def format_routing_path_network_report(diagnostic): _format_distance_mm(sample.get("terminal_access_length_mm")), ) + invalid_exit_directions = _dict_items(diagnostic.get("invalid_terminal_exit_directions", []) or []) + if invalid_exit_directions: + sample = invalid_exit_directions[0] + message += "\n端子出线方向无效:{0},字段 {1}。请检查模板端子出线方向或 QetTerminalExitDirectionJson。".format( + _diagnostic_terminal_text(sample), + sample.get("property_name", "未知字段"), + ) + invalid_local_routes = _dict_items(diagnostic.get("invalid_terminal_local_routes", []) or []) if invalid_local_routes: sample = invalid_local_routes[0] @@ -9374,7 +10510,7 @@ def format_routing_path_network_report(diagnostic): carrier_text = "、".join([str(item) for item in carriers[:3]]) if carriers else "未知 carrier" message += "\n存在孤立路径网络:{0}。请用线槽/辅助路径把孤立网络接入主网络。".format(carrier_text) - if not (empty_network or unconnected or possible_breaks or wire_duct_components or invalid_carriers or outside_carriers or outside_terminals or long_accesses or invalid_local_routes or routing_range_only or isolated): + if not (empty_network or unconnected or possible_breaks or wire_duct_components or invalid_carriers or outside_carriers or outside_terminals or long_accesses or invalid_exit_directions or invalid_local_routes or routing_range_only or isolated): first_issue = issues[0] message += "\n首个问题:{0} ({1})。".format( first_issue.get("code", "unknown"), @@ -9425,11 +10561,12 @@ def route_eplan_connections( try: routing_path_network_diagnostic = _compact_routing_path_network_diagnostic( RoutingNetwork.diagnose_routing_path_network( - doc, - terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), - terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), - terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), - adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + doc, + terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), + terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), + terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), ) ) except Exception as exc: @@ -9473,11 +10610,12 @@ def route_eplan_connections( ) routing_path_network_diagnostic = _compact_routing_path_network_diagnostic( RoutingNetwork.diagnose_routing_path_network( - doc, - terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), - terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), - terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), - adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + doc, + terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), + terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), + terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), ) ) except Exception as exc: @@ -9567,11 +10705,74 @@ def route_eplan_connections( "error": str(exc), } + auto_terminal_access_fallback_bridges = { + "enabled": bool(opts.get("auto_create_terminal_access_fallback_bridges", True)), + "targets": 0, + "created_count": 0, + "duplicates": 0, + "missing_targets": [], + "created_pair_labels": [], + "rerouted": False, + } + if bool(opts.get("auto_create_terminal_access_fallback_bridges", True)): + try: + auto_terminal_access_fallback_bridges = _create_terminal_access_fallback_bridges_from_report( + doc, + report, + project_uuid=(project_uuid or _project_uuid(doc)), + ) + if int(auto_terminal_access_fallback_bridges.get("created_count", 0) or 0) > 0: + retry_wire_uuids = _terminal_access_fallback_wire_uuids_from_report(report) + if update_network: + prepared_network = update_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=opts, + selection_ex=selection_ex, + ) + retry_payload = _payload_subset_for_wire_uuids(target_payload, retry_wire_uuids) + if isinstance(retry_payload, dict) and retry_payload.get("wires"): + retry_report = route_eplan_connections_from_payload( + doc, + retry_payload, + options=opts, + prepared_layout=prepared_network, + ) + report = _merge_retry_routes_into_report( + doc, + report, + retry_report, + retry_prefix="terminal_access_fallback", + ) + auto_terminal_access_fallback_bridges["retry_wires"] = len( + retry_payload.get("wires", []) or [] + ) + auto_terminal_access_fallback_bridges["retry_replaced_routes"] = int( + report.get("terminal_access_fallback_retry_replaced_routes", 0) or 0 + ) + auto_terminal_access_fallback_bridges["rerouted"] = True + else: + auto_terminal_access_fallback_bridges["retry_wires"] = 0 + auto_terminal_access_fallback_bridges["retry_replaced_routes"] = 0 + auto_terminal_access_fallback_bridges["rerouted"] = False + except Exception as exc: + auto_terminal_access_fallback_bridges = { + "enabled": True, + "targets": 0, + "created_count": 0, + "duplicates": 0, + "missing_targets": [], + "created_pair_labels": [], + "rerouted": False, + "error": str(exc), + } + report["routing_method"] = "eplan-route-v1" report["routing_path_network_updated"] = bool(update_network) report["routing_path_network_diagnostic"] = routing_path_network_diagnostic report["auto_diagnostic_bridges"] = auto_diagnostic_bridges report["auto_main_path_detour_bridges"] = auto_main_path_detour_bridges + report["auto_terminal_access_fallback_bridges"] = auto_terminal_access_fallback_bridges if isinstance(prepared_network, dict): report["routing_path_network"] = prepared_network if opts.get("hide_route_carriers_after_route", True): diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 8453951..8176615 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -97,6 +97,96 @@ def _style_command_button(button, text, tooltip=""): return button +def _panel_safe_int(value): + try: + return int(value or 0) + except Exception: + return 0 + + +def _panel_pair_labels(payload, limit=3): + if not isinstance(payload, dict): + return [] + labels = [] + for label in list(payload.get("created_pair_labels", []) or []): + text = str(label or "").strip() + if text: + labels.append(text) + if len(labels) >= limit: + break + return labels + + +def _panel_default_auto_bridge_enabled(options, key): + if isinstance(options, dict) and key in options: + return bool(options.get(key)) + return key in { + "auto_create_diagnostic_bridges", + "auto_create_main_path_detour_bridges", + "auto_create_terminal_access_fallback_bridges", + } + + +def _format_terminal_access_fallback_selection_status(result): + wires = _panel_safe_int((result or {}).get("selected_terminal_access_fallback_wires", 0)) + targets = _panel_safe_int((result or {}).get("selected_terminal_access_fallback_targets", 0)) + access_carriers = _panel_safe_int( + (result or {}).get("selected_terminal_access_fallback_access_carriers", 0) + ) + terminals = _panel_safe_int((result or {}).get("selected_terminal_access_fallback_terminals", 0)) + devices = _panel_safe_int((result or {}).get("selected_terminal_access_fallback_devices", 0)) + message = "已选择端子退回位置:导线 {0} 条,目标 {1} 个".format(wires, targets) + if access_carriers > 0: + message += ",接入线 {0} 条".format(access_carriers) + if terminals > 0: + message += ",端子 {0} 个".format(terminals) + if devices > 0: + message += ",设备 {0} 个".format(devices) + return message + "。" + + +def _format_wire_outside_boundary_selection_status(result): + wires = _panel_safe_int((result or {}).get("selected_wire_outside_boundary_wires", 0)) + carriers = _panel_safe_int((result or {}).get("selected_wire_outside_boundary_route_carriers", 0)) + sources = _panel_safe_int((result or {}).get("selected_wire_outside_boundary_route_sources", 0)) + message = "已选择越界导线:{0} 条".format(wires) + if carriers > 0: + message += ",路径 carrier {0} 条".format(carriers) + if sources > 0: + message += ",源对象 {0} 个".format(sources) + return message + "。" + + +def _format_route_panel_status(report): + message = AutoRouting.format_eplan_connection_route_report(report) + if not isinstance(report, dict): + return message + diagnostic = report.get("auto_diagnostic_bridges", {}) + main_path = report.get("auto_main_path_detour_bridges", {}) + terminal_access = report.get("auto_terminal_access_fallback_bridges", {}) + diagnostic_count = _panel_safe_int(diagnostic.get("created_count", 0)) if isinstance(diagnostic, dict) else 0 + main_path_count = _panel_safe_int(main_path.get("created_count", 0)) if isinstance(main_path, dict) else 0 + terminal_count = ( + _panel_safe_int(terminal_access.get("created_count", 0)) if isinstance(terminal_access, dict) else 0 + ) + if diagnostic_count <= 0 and main_path_count <= 0 and terminal_count <= 0: + return message + + # 状态栏空间有限,这里把自动修复结果提前汇总,方便手动测试时一眼确认补桥是否生效。 + message += "\n自动补桥摘要:诊断桥 {0} 条,主路径补桥 {1} 条,端子接入补桥 {2} 条。".format( + diagnostic_count, + main_path_count, + terminal_count, + ) + main_pairs = _panel_pair_labels(main_path) + if main_pairs: + message += "\n主路径配对:{0}。".format("、".join(main_pairs)) + terminal_pairs = _panel_pair_labels(terminal_access) + if terminal_pairs: + message += "\n端子接入配对:{0}。".format("、".join(terminal_pairs)) + return message + + class AutoRoutingController: def __init__(self, options=None): self.last_report = None @@ -189,6 +279,15 @@ class AutoRoutingController: sample_limit = int(AutoRouting.DEFAULT_OPTIONS["preflight_routeability_sample_limit"]) self.options["preflight_routeability_sample_limit"] = max(sample_limit, 0) + def set_auto_create_diagnostic_bridges(self, value): + self.options["auto_create_diagnostic_bridges"] = bool(value) + + def set_auto_create_main_path_detour_bridges(self, value): + self.options["auto_create_main_path_detour_bridges"] = bool(value) + + def set_auto_create_terminal_access_fallback_bridges(self, value): + self.options["auto_create_terminal_access_fallback_bridges"] = bool(value) + def summary(self): doc = _active_document() terminal_count = len(AutoRouting.index_terminals(doc)) @@ -627,17 +726,18 @@ class AutoRoutingController: source_refs.append({"name": source_name, "label": source_label}) return carrier_refs, source_refs - def _select_route_refs(self, doc, route_tracks, result_prefix): + def _select_route_refs(self, doc, route_tracks, result_prefix, clear_selection=True): selected = [] selected_names = set() selected_carriers = [] selected_sources = [] missing_refs = [] - try: - Gui.Selection.clearSelection() - except Exception: - pass + if clear_selection: + try: + Gui.Selection.clearSelection() + except Exception: + pass def add_object(obj, bucket): if obj is None: @@ -882,6 +982,155 @@ class AutoRoutingController: } return self.last_report + def select_wire_outside_boundary_wires(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + batch_payload = ( + ((summary.get("diagnostics", {}) or {}).get("RoutingConnectionBatch", {}) or {}).get("payload", {}) + if isinstance(summary.get("diagnostics", {}), dict) + else {} + ) + samples = [] + seen_refs = set() + + def add_sample(item): + if not isinstance(item, dict): + return + wire_uuid = str(item.get("wire_uuid", "") or "").strip() + wire_label = str(item.get("wire_object_label", "") or item.get("wire_label", "") or "").strip() + key = wire_uuid or wire_label + if not key or key in seen_refs: + return + seen_refs.add(key) + samples.append({"wire_uuid": wire_uuid, "wire_label": wire_label, "ref": key}) + + for sample in list(batch_payload.get("wire_outside_boundary_samples", []) or []): + add_sample(sample) + for sample in list(batch_payload.get("route_candidate_boundary_warning_samples", []) or []): + add_sample(sample) + for sample in list(batch_payload.get("route_samples", []) or []): + issue_codes = [ + str(code or "").strip() + for code in list(sample.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if "route_candidate_boundary_violations" in issue_codes or "boundary_warning" in issue_codes: + add_sample(sample) + + def find_wire(sample): + wire_uuid = sample.get("wire_uuid", "") + wire_label = sample.get("wire_label", "") + for candidate in list(getattr(doc, "Objects", []) or []): + if (getattr(candidate, "RouteType", "") or "").strip() != "RoutedConnection": + continue + if wire_uuid and str(getattr(candidate, "QetWireUuid", "") or "").strip() == wire_uuid: + return candidate + if wire_label: + for candidate in list(getattr(doc, "Objects", []) or []): + if (getattr(candidate, "RouteType", "") or "").strip() != "RoutedConnection": + continue + if str(getattr(candidate, "Label", "") or "").strip() == wire_label: + return candidate + return None + + selected = [] + selected_names = set() + missing_refs = [] + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + obj = find_wire(sample) + if obj is None: + missing_refs.append(sample.get("wire_uuid") or sample.get("wire_label") or sample.get("ref", "")) + continue + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(obj, "Name", "")) + except Exception: + continue + selected_names.add(getattr(obj, "Name", "")) + selected.append(obj) + + # 样例列表会被 limit 截断,已生成导线对象上的问题码才是完整定位来源。 + boundary_issue_codes = {"route_candidate_boundary_violations", "boundary_warning"} + for candidate in list(getattr(doc, "Objects", []) or []): + if (getattr(candidate, "RouteType", "") or "").strip() != "RoutedConnection": + continue + if getattr(candidate, "Name", "") in selected_names: + continue + issue_codes = { + code.strip() + for code in str(getattr(candidate, "QetRouteIssueCodes", "") or "").split(",") + if code.strip() + } + if not issue_codes.intersection(boundary_issue_codes): + continue + try: + Gui.Selection.addSelection(candidate) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(candidate, "Name", "")) + except Exception: + continue + selected_names.add(getattr(candidate, "Name", "")) + selected.append(candidate) + + route_tracks = [] + route_track_missing_refs = [] + for wire in selected: + route_track_text = str(getattr(wire, "QetRouteTrackJson", "") or "").strip() + if not route_track_text: + continue + try: + route_track = json.loads(route_track_text) + except Exception: + route_track_missing_refs.append(getattr(wire, "Name", "")) + continue + if isinstance(route_track, dict): + route_tracks.append(route_track) + route_ref_report = self._select_route_refs( + doc, + route_tracks, + "selected_wire_outside_boundary_route", + clear_selection=False, + ) + self.last_report = { + "selected_wire_outside_boundary_wires": len(selected), + "selected_wire_outside_boundary_wire_names": [getattr(obj, "Name", "") for obj in selected], + "selected_wire_outside_boundary_route_objects": route_ref_report.get( + "selected_wire_outside_boundary_route_objects", + 0, + ), + "selected_wire_outside_boundary_route_carriers": route_ref_report.get( + "selected_wire_outside_boundary_route_carriers", + 0, + ), + "selected_wire_outside_boundary_route_carrier_names": route_ref_report.get( + "selected_wire_outside_boundary_route_carrier_names", + [], + ), + "selected_wire_outside_boundary_route_sources": route_ref_report.get( + "selected_wire_outside_boundary_route_sources", + 0, + ), + "selected_wire_outside_boundary_route_source_names": route_ref_report.get( + "selected_wire_outside_boundary_route_source_names", + [], + ), + "missing_wire_outside_boundary_refs": missing_refs + + [ + ref + for ref in route_ref_report.get("missing_selected_wire_outside_boundary_route_refs", []) + + route_track_missing_refs + if ref and ref not in missing_refs + ], + } + return self.last_report + def select_main_path_detour_missing_wires(self): doc = _active_document() summary = AutoRouting.collect_routing_diagnostic_summary(doc) @@ -1248,57 +1497,602 @@ class AutoRoutingController: } selected = [] - selected_fallback = [] - selected_current = [] + selected_fallback = [] + selected_current = [] + selected_names = set() + missing_refs = [] + missing_current_route_refs = [] + try: + Gui.Selection.clearSelection() + except Exception: + pass + def select_refs_for_label(label, missing_bucket, selected_bucket): + matches = self._find_objects_by_name_or_label(doc, name=label, label=label) + if not matches: + missing_bucket.append(label) + return + for obj in matches: + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + continue + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) + except Exception: + continue + selected_names.add(obj_name) + selected.append(obj) + selected_bucket.append(obj) + + for label in labels: + select_refs_for_label(label, missing_refs, selected_fallback) + for label in current_route_labels: + select_refs_for_label(label, missing_current_route_refs, selected_current) + self.last_report = { + "selected_main_path_detour_bridge_endpoint_objects": len(selected), + "selected_main_path_detour_rejected_fallback_sources": len(selected_fallback), + "selected_main_path_detour_current_route_sources": len(selected_current), + "selected_main_path_detour_rejected_fallback_source_names": [ + getattr(obj, "Name", "") for obj in selected_fallback + ], + "selected_main_path_detour_current_route_source_names": [ + getattr(obj, "Name", "") for obj in selected_current + ], + "main_path_detour_rejected_fallback_labels": labels, + "main_path_detour_rejected_fallback_label_counts": label_counts, + "main_path_detour_rejected_fallback_kind_counts": kind_counts, + "main_path_detour_current_route_source_labels": current_route_labels, + "main_path_detour_current_route_source_label_counts": current_route_label_counts, + "main_path_detour_bridge_pair_counts": bridge_pair_counts, + "missing_main_path_detour_rejected_fallback_refs": missing_refs, + "missing_main_path_detour_current_route_refs": missing_current_route_refs, + } + return self.last_report + + def select_terminal_access_fallback_targets(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + batch_payload = ( + ((summary.get("diagnostics", {}) or {}).get("RoutingConnectionBatch", {}) or {}).get("payload", {}) + if isinstance(summary.get("diagnostics", {}), dict) + else {} + ) + samples = list(batch_payload.get("terminal_access_fallback_target_samples", []) or []) + if not samples and isinstance(summary.get("diagnostics", {}), dict): + path_payload = ( + ((summary.get("diagnostics", {}) or {}).get("RoutingPathNetwork", {}) or {}).get("payload", {}) + ) + if isinstance(path_payload, dict): + samples = list(path_payload.get("terminal_access_fallback_targets", []) or []) + selected = [] + selected_wires = [] + selected_targets = [] + selected_access_carriers = [] + selected_terminals = [] + selected_devices = [] + selected_names = set() + missing_refs = [] + + def add_selection(obj, bucket): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + return True + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) + except Exception: + return False + selected_names.add(obj_name) + selected.append(obj) + bucket.append(obj) + return True + + def find_wire(wire_uuid): + wire_uuid = str(wire_uuid or "").strip() + if not wire_uuid: + return None + for candidate in list(getattr(doc, "Objects", []) or []): + if (getattr(candidate, "RouteType", "") or "").strip() != "RoutedConnection": + continue + if str(getattr(candidate, "QetWireUuid", "") or "").strip() == wire_uuid: + return candidate + return None + + def terminal_access_carrier_from_wire(wire, endpoint): + if wire is None: + return None + endpoint = str(endpoint or "").strip().lower() + if endpoint not in {"start", "end"}: + return None + try: + network_payload = json.loads(str(getattr(wire, "QetRouteNetworkJson", "") or "{}")) + except Exception: + network_payload = {} + if not isinstance(network_payload, dict): + return None + carrier_name = str( + network_payload.get("{0}_terminal_access_carrier".format(endpoint), "") or "" + ).strip() + if not carrier_name: + return None + try: + return doc.getObject(carrier_name) + except Exception: + return None + + def terminal_access_carrier_from_sample(sample): + carrier = self._find_object_by_name_or_label( + doc, + sample.get("access_carrier_name", ""), + sample.get("access_carrier_label", ""), + ) + if carrier is not None: + return carrier + return terminal_access_carrier_from_wire(sample.get("_wire"), sample.get("endpoint", "")) + + def find_terminal(sample): + terminal_name = str(sample.get("terminal_name", "") or "").strip() + terminal_uuid = str(sample.get("terminal_uuid", "") or "").strip() + terminal_label = str(sample.get("terminal_label", "") or "").strip() + if terminal_name: + obj = doc.getObject(terminal_name) + if obj is not None: + return obj + for candidate in list(getattr(doc, "Objects", []) or []): + if terminal_uuid and str(getattr(candidate, "QetTerminalUuid", "") or "").strip() == terminal_uuid: + return candidate + if terminal_label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == terminal_label: + return candidate + return None + + def find_parent_device(sample): + device_name = str(sample.get("parent_device_name", "") or "").strip() + device_label = str(sample.get("parent_device_label", "") or "").strip() + return self._find_object_by_name_or_label(doc, device_name, device_label) + + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + wire_ref = str(sample.get("wire_uuid", "") or sample.get("wire_label", "") or "").strip() + wire = find_wire(sample.get("wire_uuid", "")) + sample["_wire"] = wire + if wire is None: + if wire_ref and wire_ref not in missing_refs: + missing_refs.append(wire_ref) + else: + add_selection(wire, selected_wires) + + target_name = str(sample.get("target_name", "") or "").strip() + target_label = str(sample.get("target_label", "") or "").strip() + target_kind = str(sample.get("target_kind", "") or "").strip() + target_matches = self._find_objects_by_name_or_label(doc, name=target_name, label="") + if not target_matches: + target_matches = self._find_objects_by_name_or_label(doc, name=target_label, label=target_label) + if not target_matches: + missing_ref = target_name or target_label or target_kind + if missing_ref and missing_ref not in missing_refs: + missing_refs.append(missing_ref) + else: + for target in target_matches: + add_selection(target, selected_targets) + + access_carrier = terminal_access_carrier_from_sample(sample) + if access_carrier is not None: + add_selection(access_carrier, selected_access_carriers) + elif sample.get("access_carrier_name", "") or sample.get("access_carrier_label", ""): + missing_ref = sample.get("access_carrier_name", "") or sample.get("access_carrier_label", "") + if missing_ref and missing_ref not in missing_refs: + missing_refs.append(missing_ref) + terminal = find_terminal(sample) + if terminal is not None: + add_selection(terminal, selected_terminals) + elif sample.get("terminal_name", "") or sample.get("terminal_label", "") or sample.get("terminal_uuid", ""): + missing_ref = sample.get("terminal_name", "") or sample.get("terminal_label", "") or sample.get("terminal_uuid", "") + if missing_ref and missing_ref not in missing_refs: + missing_refs.append(missing_ref) + parent_device = find_parent_device(sample) + if parent_device is not None: + add_selection(parent_device, selected_devices) + elif sample.get("parent_device_name", "") or sample.get("parent_device_label", ""): + missing_ref = sample.get("parent_device_name", "") or sample.get("parent_device_label", "") + if missing_ref and missing_ref not in missing_refs: + missing_refs.append(missing_ref) + + # 诊断样例会被显示数量截断;已生成导线对象上的问题码用于补全选择。 + for candidate in list(getattr(doc, "Objects", []) or []): + if (getattr(candidate, "RouteType", "") or "").strip() != "RoutedConnection": + continue + if getattr(candidate, "Name", "") in selected_names: + continue + issue_codes = { + code.strip() + for code in str(getattr(candidate, "QetRouteIssueCodes", "") or "").split(",") + if code.strip() + } + if "terminal_access_fallback_targets" not in issue_codes: + continue + add_selection(candidate, selected_wires) + + self.last_report = { + "selected_terminal_access_fallback_objects": len(selected), + "selected_terminal_access_fallback_wires": len(selected_wires), + "selected_terminal_access_fallback_targets": len(selected_targets), + "selected_terminal_access_fallback_access_carriers": len(selected_access_carriers), + "selected_terminal_access_fallback_terminals": len(selected_terminals), + "selected_terminal_access_fallback_devices": len(selected_devices), + "selected_terminal_access_fallback_wire_names": [ + getattr(obj, "Name", "") for obj in selected_wires + ], + "selected_terminal_access_fallback_target_names": [ + getattr(obj, "Name", "") for obj in selected_targets + ], + "selected_terminal_access_fallback_access_carrier_names": [ + getattr(obj, "Name", "") for obj in selected_access_carriers + ], + "selected_terminal_access_fallback_terminal_names": [ + getattr(obj, "Name", "") for obj in selected_terminals + ], + "selected_terminal_access_fallback_device_names": [ + getattr(obj, "Name", "") for obj in selected_devices + ], + "missing_terminal_access_fallback_refs": missing_refs, + } + return self.last_report + + def select_terminal_access_endpoint_device_avoidance(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + path_payload = ( + ((summary.get("diagnostics", {}) or {}).get("RoutingPathNetwork", {}) or {}).get("payload", {}) + if isinstance(summary.get("diagnostics", {}), dict) + else {} + ) + samples = list(path_payload.get("terminal_access_endpoint_device_avoidance", []) or []) + selected = [] + selected_terminals = [] + selected_devices = [] + selected_targets = [] + selected_access_carriers = [] + selected_names = set() + missing_refs = [] + + def add_selection(obj, bucket): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + return True + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) + except Exception: + return False + selected_names.add(obj_name) + selected.append(obj) + bucket.append(obj) + return True + + def remember_missing(ref): + ref = str(ref or "").strip() + if ref and ref not in missing_refs: + missing_refs.append(ref) + + def find_terminal(sample): + terminal_name = str(sample.get("terminal_name", "") or "").strip() + terminal_uuid = str(sample.get("terminal_uuid", "") or "").strip() + terminal_label = str(sample.get("terminal_label", "") or "").strip() + if terminal_name: + obj = doc.getObject(terminal_name) + if obj is not None: + return obj + for candidate in list(getattr(doc, "Objects", []) or []): + if terminal_uuid and str(getattr(candidate, "QetTerminalUuid", "") or "").strip() == terminal_uuid: + return candidate + if terminal_label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == terminal_label: + return candidate + return None + + def find_one_by_name_or_label(name, label): + matches = self._find_objects_by_name_or_label(doc, name=name, label="") + if not matches: + matches = self._find_objects_by_name_or_label(doc, name=label, label=label) + return matches[0] if matches else None + + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + # 端点设备避让样本用于验收定位:先选端子/设备,再选接入目标和 TerminalAccess 接入段。 + terminal = find_terminal(sample) + if terminal is None: + remember_missing( + sample.get("terminal_name", "") + or sample.get("terminal_label", "") + or sample.get("terminal_uuid", "") + ) + else: + add_selection(terminal, selected_terminals) + + parent_device = self._find_object_by_name_or_label( + doc, + sample.get("parent_device_name", ""), + sample.get("parent_device_label", ""), + ) + if parent_device is None: + remember_missing(sample.get("parent_device_name", "") or sample.get("parent_device_label", "")) + else: + add_selection(parent_device, selected_devices) + + target = find_one_by_name_or_label(sample.get("target_name", ""), sample.get("target_label", "")) + if target is None: + remember_missing(sample.get("target_name", "") or sample.get("target_label", "")) + else: + add_selection(target, selected_targets) + + access_carrier = find_one_by_name_or_label( + sample.get("access_carrier_name", ""), + sample.get("access_carrier_label", ""), + ) + if access_carrier is None: + remember_missing(sample.get("access_carrier_name", "") or sample.get("access_carrier_label", "")) + else: + add_selection(access_carrier, selected_access_carriers) + + self.last_report = { + "selected_terminal_access_endpoint_avoidance_objects": len(selected), + "selected_terminal_access_endpoint_avoidance_terminals": len(selected_terminals), + "selected_terminal_access_endpoint_avoidance_devices": len(selected_devices), + "selected_terminal_access_endpoint_avoidance_targets": len(selected_targets), + "selected_terminal_access_endpoint_avoidance_access_carriers": len(selected_access_carriers), + "selected_terminal_access_endpoint_avoidance_terminal_names": [ + getattr(obj, "Name", "") for obj in selected_terminals + ], + "selected_terminal_access_endpoint_avoidance_device_names": [ + getattr(obj, "Name", "") for obj in selected_devices + ], + "selected_terminal_access_endpoint_avoidance_target_names": [ + getattr(obj, "Name", "") for obj in selected_targets + ], + "selected_terminal_access_endpoint_avoidance_access_carrier_names": [ + getattr(obj, "Name", "") for obj in selected_access_carriers + ], + "missing_terminal_access_endpoint_avoidance_refs": missing_refs, + } + return self.last_report + + def select_unconnected_terminal_access_issues(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + path_payload = ( + ((summary.get("diagnostics", {}) or {}).get("RoutingPathNetwork", {}) or {}).get("payload", {}) + if isinstance(summary.get("diagnostics", {}), dict) + else {} + ) + samples = list(path_payload.get("unconnected_terminals", []) or []) if isinstance(path_payload, dict) else [] + selected = [] + selected_terminals = [] + selected_devices = [] + selected_names = set() + missing_refs = [] + max_distance = 0.0 + + def add_selection(obj, bucket): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + return True + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) + except Exception: + return False + selected_names.add(obj_name) + selected.append(obj) + bucket.append(obj) + return True + + def remember_missing(ref): + ref = str(ref or "").strip() + if ref and ref not in missing_refs: + missing_refs.append(ref) + + def find_terminal(sample): + terminal_name = str(sample.get("name", "") or sample.get("terminal_name", "") or "").strip() + terminal_uuid = str(sample.get("terminal_uuid", "") or "").strip() + terminal_label = str(sample.get("label", "") or sample.get("terminal_label", "") or "").strip() + if terminal_name: + obj = doc.getObject(terminal_name) + if obj is not None: + return obj + for candidate in list(getattr(doc, "Objects", []) or []): + if terminal_uuid and str(getattr(candidate, "QetTerminalUuid", "") or "").strip() == terminal_uuid: + return candidate + if terminal_label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == terminal_label: + return candidate + return None + + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + try: + max_distance = max(max_distance, float(sample.get("nearest_network_distance_mm", 0.0) or 0.0)) + except Exception: + pass + terminal = find_terminal(sample) + if terminal is None: + remember_missing( + sample.get("name", "") + or sample.get("terminal_name", "") + or sample.get("terminal_uuid", "") + or sample.get("label", "") + ) + else: + add_selection(terminal, selected_terminals) + + parent_device = self._find_object_by_name_or_label( + doc, + sample.get("parent_device_name", ""), + sample.get("parent_device_label", ""), + ) + if parent_device is None: + remember_missing(sample.get("parent_device_name", "") or sample.get("parent_device_label", "")) + else: + add_selection(parent_device, selected_devices) + + self.last_report = { + "selected_unconnected_terminal_access_objects": len(selected), + "selected_unconnected_terminal_access_terminals": len(selected_terminals), + "selected_unconnected_terminal_access_devices": len(selected_devices), + "selected_unconnected_terminal_access_terminal_names": [ + getattr(obj, "Name", "") for obj in selected_terminals + ], + "selected_unconnected_terminal_access_device_names": [ + getattr(obj, "Name", "") for obj in selected_devices + ], + "missing_unconnected_terminal_access_refs": missing_refs, + "max_unconnected_terminal_access_distance_mm": float(max_distance), + } + return self.last_report + + def select_terminal_exit_issue_terminals(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + path_payload = ( + ((summary.get("diagnostics", {}) or {}).get("RoutingPathNetwork", {}) or {}).get("payload", {}) + if isinstance(summary.get("diagnostics", {}), dict) + else {} + ) + issue_groups = ( + ("corrected", list(path_payload.get("corrected_terminal_exits", []) or [])), + ("capped", list(path_payload.get("capped_terminal_exits", []) or [])), + ("invalid_direction", list(path_payload.get("invalid_terminal_exit_directions", []) or [])), + ("invalid_local_route", list(path_payload.get("invalid_terminal_local_routes", []) or [])), + ) + selected = [] + selected_terminals = [] + selected_devices = [] selected_names = set() missing_refs = [] - missing_current_route_refs = [] + issue_terminal_counts = { + "corrected": 0, + "capped": 0, + "invalid_direction": 0, + "invalid_local_route": 0, + } + + def add_selection(obj, bucket): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + return True + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) + except Exception: + return False + selected_names.add(obj_name) + selected.append(obj) + bucket.append(obj) + return True + + def remember_missing(ref): + ref = str(ref or "").strip() + if ref and ref not in missing_refs: + missing_refs.append(ref) + + def find_terminal(sample): + terminal_name = str(sample.get("name", "") or sample.get("terminal_name", "") or "").strip() + terminal_uuid = str(sample.get("terminal_uuid", "") or "").strip() + terminal_label = str(sample.get("label", "") or sample.get("terminal_label", "") or "").strip() + if terminal_name: + obj = doc.getObject(terminal_name) + if obj is not None: + return obj + for candidate in list(getattr(doc, "Objects", []) or []): + if terminal_uuid and str(getattr(candidate, "QetTerminalUuid", "") or "").strip() == terminal_uuid: + return candidate + if terminal_label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == terminal_label: + return candidate + return None + try: Gui.Selection.clearSelection() except Exception: pass - def select_refs_for_label(label, missing_bucket, selected_bucket): - matches = self._find_objects_by_name_or_label(doc, name=label, label=label) - if not matches: - missing_bucket.append(label) - return - for obj in matches: - obj_name = getattr(obj, "Name", "") - if obj_name in selected_names: + for issue_kind, samples in issue_groups: + for sample in samples: + if not isinstance(sample, dict): continue - try: - Gui.Selection.addSelection(obj) - except Exception: - try: - Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) - except Exception: - continue - selected_names.add(obj_name) - selected.append(obj) - selected_bucket.append(obj) + terminal = find_terminal(sample) + if terminal is None: + remember_missing( + sample.get("name", "") + or sample.get("terminal_name", "") + or sample.get("terminal_uuid", "") + or sample.get("label", "") + ) + else: + if add_selection(terminal, selected_terminals): + issue_terminal_counts[issue_kind] += 1 + + parent_device = self._find_object_by_name_or_label( + doc, + sample.get("parent_device_name", ""), + sample.get("parent_device_label", ""), + ) + if parent_device is None: + remember_missing(sample.get("parent_device_name", "") or sample.get("parent_device_label", "")) + else: + add_selection(parent_device, selected_devices) - for label in labels: - select_refs_for_label(label, missing_refs, selected_fallback) - for label in current_route_labels: - select_refs_for_label(label, missing_current_route_refs, selected_current) self.last_report = { - "selected_main_path_detour_bridge_endpoint_objects": len(selected), - "selected_main_path_detour_rejected_fallback_sources": len(selected_fallback), - "selected_main_path_detour_current_route_sources": len(selected_current), - "selected_main_path_detour_rejected_fallback_source_names": [ - getattr(obj, "Name", "") for obj in selected_fallback + "selected_terminal_exit_issue_objects": len(selected), + "selected_terminal_exit_issue_terminals": len(selected_terminals), + "selected_terminal_exit_issue_devices": len(selected_devices), + "selected_terminal_exit_corrected_terminals": issue_terminal_counts["corrected"], + "selected_terminal_exit_capped_terminals": issue_terminal_counts["capped"], + "selected_terminal_exit_invalid_direction_terminals": issue_terminal_counts["invalid_direction"], + "selected_terminal_exit_invalid_local_route_terminals": issue_terminal_counts["invalid_local_route"], + "selected_terminal_exit_issue_terminal_names": [ + getattr(obj, "Name", "") for obj in selected_terminals ], - "selected_main_path_detour_current_route_source_names": [ - getattr(obj, "Name", "") for obj in selected_current + "selected_terminal_exit_issue_device_names": [ + getattr(obj, "Name", "") for obj in selected_devices ], - "main_path_detour_rejected_fallback_labels": labels, - "main_path_detour_rejected_fallback_label_counts": label_counts, - "main_path_detour_rejected_fallback_kind_counts": kind_counts, - "main_path_detour_current_route_source_labels": current_route_labels, - "main_path_detour_current_route_source_label_counts": current_route_label_counts, - "main_path_detour_bridge_pair_counts": bridge_pair_counts, - "missing_main_path_detour_rejected_fallback_refs": missing_refs, - "missing_main_path_detour_current_route_refs": missing_current_route_refs, + "missing_terminal_exit_issue_refs": missing_refs, } return self.last_report @@ -1337,18 +2131,28 @@ class AutoRoutingController: seen_names.add(candidate_name) return refs - def select_long_terminal_accesses(self): - doc = _active_document() - summary = AutoRouting.collect_routing_diagnostic_summary(doc) - batch_payload = ( - ((summary.get("diagnostics", {}) or {}).get("RoutingConnectionBatch", {}) or {}).get("payload", {}) - if isinstance(summary.get("diagnostics", {}), dict) - else {} - ) + @staticmethod + def _long_terminal_access_samples_from_summary(summary): + diagnostics = summary.get("diagnostics", {}) if isinstance(summary, dict) else {} + if not isinstance(diagnostics, dict): + diagnostics = {} + path_payload = ((diagnostics.get("RoutingPathNetwork", {}) or {}).get("payload", {})) + if isinstance(path_payload, dict): + samples = list(path_payload.get("long_terminal_accesses", []) or []) + if samples: + return samples + batch_payload = ((diagnostics.get("RoutingConnectionBatch", {}) or {}).get("payload", {})) + if not isinstance(batch_payload, dict): + return [] path_diagnostic = batch_payload.get("routing_path_network_diagnostic", {}) if not isinstance(path_diagnostic, dict): path_diagnostic = {} - samples = list(path_diagnostic.get("long_terminal_accesses", []) or []) + return list(path_diagnostic.get("long_terminal_accesses", []) or []) + + def select_long_terminal_accesses(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + samples = self._long_terminal_access_samples_from_summary(summary) def find_terminal(item): terminal_uuid = str(item.get("terminal_uuid", "") or "").strip() @@ -1400,15 +2204,7 @@ class AutoRoutingController: def select_long_terminal_access_devices(self): doc = _active_document() summary = AutoRouting.collect_routing_diagnostic_summary(doc) - batch_payload = ( - ((summary.get("diagnostics", {}) or {}).get("RoutingConnectionBatch", {}) or {}).get("payload", {}) - if isinstance(summary.get("diagnostics", {}), dict) - else {} - ) - path_diagnostic = batch_payload.get("routing_path_network_diagnostic", {}) - if not isinstance(path_diagnostic, dict): - path_diagnostic = {} - samples = list(path_diagnostic.get("long_terminal_accesses", []) or []) + samples = self._long_terminal_access_samples_from_summary(summary) selected = [] selected_names = set() missing_refs = [] @@ -1944,6 +2740,19 @@ class AutoRoutingController: } return self.last_report + def set_selected_terminal_exit_direction(self): + result = RoutingNetwork.set_terminal_exit_direction_from_selection(_selection_ex()) + terminal = result.get("terminal") + self.last_report = { + "terminal_exit_directions": 1, + "terminal_exit_direction_names": [getattr(terminal, "Name", "")] if terminal is not None else [], + "terminal_exit_direction_labels": [getattr(terminal, "Label", "")] if terminal is not None else [], + "terminal_exit_direction": dict(result.get("direction", {}) or {}), + "terminal_exit_direction_point_count": int(result.get("point_count", 0) or 0), + "property_name": result.get("property_name", ""), + } + return self.last_report + def create_user_path_bridge_from_selection(self): doc = _active_document() project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() @@ -2010,6 +2819,37 @@ class AutoRoutingController: } return self.last_report + def create_user_path_bridges_from_terminal_access_fallback_targets(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + report_payload = {} + diagnostics = summary.get("diagnostics", {}) if isinstance(summary, dict) else {} + if isinstance(diagnostics, dict): + batch_payload = ((diagnostics.get("RoutingConnectionBatch", {}) or {}).get("payload", {}) or {}) + report_payload = batch_payload if isinstance(batch_payload, dict) else {} + if not list(report_payload.get("terminal_access_fallback_target_samples", []) or []): + path_payload = ((diagnostics.get("RoutingPathNetwork", {}) or {}).get("payload", {}) or {}) + if isinstance(path_payload, dict) and list(path_payload.get("terminal_access_fallback_targets", []) or []): + # 只检查路径网络时还没有导线批量报告,也要能按诊断结果补 TerminalAccess 桥。 + report_payload = path_payload + bridge_report = AutoRouting._create_terminal_access_fallback_bridges_from_report( + doc, + report_payload, + project_uuid=project_uuid, + ) + + self.last_report = { + "terminal_access_fallback_bridge_targets": int(bridge_report.get("targets", 0) or 0), + "terminal_access_fallback_user_path_bridges": int(bridge_report.get("created_count", 0) or 0), + "terminal_access_fallback_bridge_duplicates": int(bridge_report.get("duplicates", 0) or 0), + "missing_terminal_access_fallback_bridge_refs": list(bridge_report.get("missing_targets", []) or []), + "terminal_access_fallback_bridge_pair_labels": list( + bridge_report.get("created_pair_labels", []) or [] + ), + } + return self.last_report + def create_user_path_bridges_from_diagnostic_suggestions(self): doc = _active_document() project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() @@ -2041,8 +2881,10 @@ class AutoRoutingController: break detour_bridge_report = self.create_user_path_bridges_from_main_path_detour_pairs() detour_created = int(detour_bridge_report.get("main_path_detour_user_path_bridges", 0) or 0) + fallback_bridge_report = self.create_user_path_bridges_from_terminal_access_fallback_targets() + fallback_created = int(fallback_bridge_report.get("terminal_access_fallback_user_path_bridges", 0) or 0) self.last_report = { - "user_path_bridges": total_created + detour_created, + "user_path_bridges": total_created + detour_created + fallback_created, "diagnostic_suggestions": total_suggestions, "duplicate_bridges": total_duplicates, "stale_suggestions": total_stale, @@ -2053,6 +2895,19 @@ class AutoRoutingController: "missing_main_path_detour_bridge_pairs": list( detour_bridge_report.get("missing_main_path_detour_bridge_pairs", []) or [] ), + "terminal_access_fallback_bridge_targets": int( + fallback_bridge_report.get("terminal_access_fallback_bridge_targets", 0) or 0 + ), + "terminal_access_fallback_user_path_bridges": fallback_created, + "terminal_access_fallback_bridge_duplicates": int( + fallback_bridge_report.get("terminal_access_fallback_bridge_duplicates", 0) or 0 + ), + "missing_terminal_access_fallback_bridge_refs": list( + fallback_bridge_report.get("missing_terminal_access_fallback_bridge_refs", []) or [] + ), + "terminal_access_fallback_bridge_pair_labels": list( + fallback_bridge_report.get("terminal_access_fallback_bridge_pair_labels", []) or [] + ), "routing_path_network_checked": True, "network": RoutingNetwork.network_summary( doc, @@ -2428,6 +3283,43 @@ class AutoRoutingTaskPanel: ) options_layout.addWidget(self.user_path_sketch_offset_spin) + auto_options_layout = QtWidgets.QHBoxLayout() + self.auto_create_diagnostic_bridges_check = QtWidgets.QCheckBox("自动诊断桥接") + self.auto_create_diagnostic_bridges_check.setToolTip( + "生成布线连接前,按路径网络诊断建议自动生成线槽/端子网络桥接 UserPath。" + ) + self.auto_create_diagnostic_bridges_check.setChecked( + _panel_default_auto_bridge_enabled( + self.controller.routing_options(), + "auto_create_diagnostic_bridges", + ) + ) + auto_options_layout.addWidget(self.auto_create_diagnostic_bridges_check) + + self.auto_create_main_path_detour_bridges_check = QtWidgets.QCheckBox("自动缺主路径补桥") + self.auto_create_main_path_detour_bridges_check.setToolTip( + "首轮布线发现导线只能退回布线面/辅助路径时,自动把兜底区域桥接到当前主路径并重跑相关导线。" + ) + self.auto_create_main_path_detour_bridges_check.setChecked( + _panel_default_auto_bridge_enabled( + self.controller.routing_options(), + "auto_create_main_path_detour_bridges", + ) + ) + auto_options_layout.addWidget(self.auto_create_main_path_detour_bridges_check) + + self.auto_create_terminal_access_fallback_bridges_check = QtWidgets.QCheckBox("自动端子接入补桥") + self.auto_create_terminal_access_fallback_bridges_check.setToolTip( + "首轮布线发现端子接入退回布线面/辅助路径时,自动桥接到最近主路径并重跑相关导线。" + ) + self.auto_create_terminal_access_fallback_bridges_check.setChecked( + _panel_default_auto_bridge_enabled( + self.controller.routing_options(), + "auto_create_terminal_access_fallback_bridges", + ) + ) + auto_options_layout.addWidget(self.auto_create_terminal_access_fallback_bridges_check) + self.generate_layout_button = _style_command_button( QtWidgets.QPushButton(), "准备布线布局空间", @@ -2470,6 +3362,12 @@ class AutoRoutingTaskPanel: "选中一个可布线端子和一条草图/Draft 局部路径,把路径写入端子的 QetTerminalLocalRoutePointsJson;不写数据库。", ) + self.set_terminal_exit_direction_button = _style_command_button( + QtWidgets.QPushButton(), + "选中端子设置出线方向", + "选中一个可布线端子和一条草图/Draft 方向线,把归一化方向写入 QetTerminalExitDirectionJson;不写数据库。", + ) + self.create_user_path_bridge_button = _style_command_button( QtWidgets.QPushButton(), "选中两路径生成桥接", @@ -2479,7 +3377,7 @@ class AutoRoutingTaskPanel: self.create_diagnostic_bridges_button = _style_command_button( QtWidgets.QPushButton(), "按诊断建议生成桥接", - "先刷新布线路径网络诊断,再按 wire_ducts_without_terminal_access 的 bridge_suggestion 生成 UserPath 桥接。", + "按路径网络诊断、缺主路径配对和端子接入退回目标生成 UserPath 桥接;不写数据库。", ) self.mark_cabinet_boundary_button = _style_command_button( @@ -2494,6 +3392,12 @@ class AutoRoutingTaskPanel: "从最新路径网络诊断中选择越出 CabinetInterior 的路径 carrier 和工程端子,便于修正 UserPath、边界或设备位置。", ) + self.select_wire_outside_boundary_wires_button = _style_command_button( + QtWidgets.QPushButton(), + "选择越界导线", + "从最新批量布线诊断中选择最终路径越出柜内区域的导线,便于补柜内 UserPath/线槽或修正柜内边界。", + ) + self.mark_pass_through_obstacle_button = _style_command_button( QtWidgets.QPushButton(), "选中对象忽略碰撞", @@ -2554,6 +3458,30 @@ class AutoRoutingTaskPanel: "从汇总诊断的需补路径位置中反选被拒绝的 RoutingRange/AuxiliaryPath 来源对象。", ) + self.select_terminal_access_fallback_targets_button = _style_command_button( + QtWidgets.QPushButton(), + "选择端子退回位置", + "从最新路径网络或批量布线诊断中选择端子接入退回布线面/辅助路径的端子、接入段和目标对象。", + ) + + self.select_terminal_access_endpoint_avoidance_button = _style_command_button( + QtWidgets.QPushButton(), + "选择端点避让接入", + "从最新路径网络诊断中选择发生端点设备避让的端子、设备、主路径和 TerminalAccess 接入段。", + ) + + self.select_unconnected_terminal_access_button = _style_command_button( + QtWidgets.QPushButton(), + "选择未接入端子", + "从最新路径网络诊断中选择未接入路由网络或接入距离超过上限的端子及所属设备。", + ) + + self.select_terminal_exit_issue_terminals_button = _style_command_button( + QtWidgets.QPushButton(), + "选择出线问题端子", + "从最新路径网络诊断中选择默认出线方向被校正或出线长度被截断的端子和所属设备。", + ) + self.select_issue_wires_button = _style_command_button( QtWidgets.QPushButton(), "选择异常导线", @@ -2682,10 +3610,12 @@ class AutoRoutingTaskPanel: self.create_orthogonal_3d_user_path_button, self.create_user_paths_button, self.set_terminal_local_route_button, + self.set_terminal_exit_direction_button, self.create_user_path_bridge_button, self.create_diagnostic_bridges_button, self.mark_cabinet_boundary_button, self.select_boundary_issue_objects_button, + self.select_wire_outside_boundary_wires_button, self.select_top_collision_obstacles_button, self.select_device_collision_obstacles_button, self.select_collision_parent_assemblies_button, @@ -2695,6 +3625,10 @@ class AutoRoutingTaskPanel: self.select_main_path_detour_missing_wires_button, self.select_main_path_detour_missing_route_sources_button, self.select_main_path_detour_rejected_fallback_sources_button, + self.select_terminal_access_fallback_targets_button, + self.select_terminal_access_endpoint_avoidance_button, + self.select_unconnected_terminal_access_button, + self.select_terminal_exit_issue_terminals_button, self.select_issue_wires_button, self.select_issue_route_sources_button, self.select_selected_wire_route_sources_button, @@ -2723,6 +3657,7 @@ class AutoRoutingTaskPanel: layout.addWidget(widget) layout.addLayout(options_layout) + layout.addLayout(auto_options_layout) layout.addWidget(self.status_label) self.generate_paths_button.clicked.connect(self.generate_routing_paths) @@ -2731,10 +3666,12 @@ class AutoRoutingTaskPanel: self.create_orthogonal_3d_user_path_button.clicked.connect(self.create_orthogonal_user_path_from_selected_points) self.create_user_paths_button.clicked.connect(self.create_user_paths_from_selection) self.set_terminal_local_route_button.clicked.connect(self.set_selected_terminal_local_route_points) + self.set_terminal_exit_direction_button.clicked.connect(self.set_selected_terminal_exit_direction) self.create_user_path_bridge_button.clicked.connect(self.create_user_path_bridge_from_selection) self.create_diagnostic_bridges_button.clicked.connect(self.create_user_path_bridges_from_diagnostic_suggestions) self.mark_cabinet_boundary_button.clicked.connect(self.mark_cabinet_boundary_from_selection) self.select_boundary_issue_objects_button.clicked.connect(self.select_boundary_issue_route_carriers_and_terminals) + self.select_wire_outside_boundary_wires_button.clicked.connect(self.select_wire_outside_boundary_wires) self.select_top_collision_obstacles_button.clicked.connect(self.select_top_collision_obstacles) self.select_device_collision_obstacles_button.clicked.connect(self.select_device_or_layout_collision_obstacles) self.select_collision_parent_assemblies_button.clicked.connect(self.select_top_collision_parent_assemblies) @@ -2752,6 +3689,18 @@ class AutoRoutingTaskPanel: self.select_main_path_detour_rejected_fallback_sources_button.clicked.connect( self.select_main_path_detour_rejected_fallback_sources ) + self.select_terminal_access_fallback_targets_button.clicked.connect( + self.select_terminal_access_fallback_targets + ) + self.select_terminal_access_endpoint_avoidance_button.clicked.connect( + self.select_terminal_access_endpoint_device_avoidance + ) + self.select_unconnected_terminal_access_button.clicked.connect( + self.select_unconnected_terminal_access_issues + ) + self.select_terminal_exit_issue_terminals_button.clicked.connect( + self.select_terminal_exit_issue_terminals + ) self.select_issue_wires_button.clicked.connect(self.select_issue_wires) self.select_issue_route_sources_button.clicked.connect(self.select_issue_route_sources) self.select_selected_wire_route_sources_button.clicked.connect(self.select_selected_wire_route_sources) @@ -2812,6 +3761,13 @@ class AutoRoutingTaskPanel: self.controller.set_lane_axis(self.lane_axis_combo.currentText()) self.controller.set_selected_route_capacity(self.selected_route_capacity_spin.value()) self.controller.set_user_path_sketch_offset(self.user_path_sketch_offset_spin.value()) + self.controller.set_auto_create_diagnostic_bridges(self.auto_create_diagnostic_bridges_check.isChecked()) + self.controller.set_auto_create_main_path_detour_bridges( + self.auto_create_main_path_detour_bridges_check.isChecked() + ) + self.controller.set_auto_create_terminal_access_fallback_bridges( + self.auto_create_terminal_access_fallback_bridges_check.isChecked() + ) def generate_routing_paths(self): try: @@ -2979,6 +3935,31 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def set_selected_terminal_exit_direction(self): + try: + result = self.controller.set_selected_terminal_exit_direction() + count = result.get("terminal_exit_directions", 0) + labels = list(result.get("terminal_exit_direction_labels", []) or []) + display = labels[0] if labels else "" + direction = result.get("terminal_exit_direction", {}) or {} + if count <= 0: + self._set_status( + "未设置端子出线方向。请同时选中一个可布线端子和一条草图/Draft 方向线。" + + self.controller.summary() + ) + return + self._set_status( + "已设置端子出线方向:{0} -> ({1:.3f}, {2:.3f}, {3:.3f})。重新生成布线路径网络后生效。{4}".format( + display or "选中端子", + float(direction.get("x", 0.0) or 0.0), + float(direction.get("y", 0.0) or 0.0), + float(direction.get("z", 0.0) or 0.0), + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + def create_user_path_bridge_from_selection(self): try: self._sync_options_from_widgets() @@ -3008,6 +3989,10 @@ class AutoRoutingTaskPanel: detour_created = result.get("main_path_detour_user_path_bridges", 0) detour_duplicates = result.get("main_path_detour_bridge_duplicates", 0) missing_detour_pairs = list(result.get("missing_main_path_detour_bridge_pairs", []) or []) + terminal_fallback_targets = result.get("terminal_access_fallback_bridge_targets", 0) + terminal_fallback_created = result.get("terminal_access_fallback_user_path_bridges", 0) + terminal_fallback_duplicates = result.get("terminal_access_fallback_bridge_duplicates", 0) + missing_terminal_fallback_refs = list(result.get("missing_terminal_access_fallback_bridge_refs", []) or []) if created <= 0: detour_text = "" if detour_pairs: @@ -3015,15 +4000,26 @@ class AutoRoutingTaskPanel: int(detour_pairs or 0), int(detour_duplicates or 0), ) + terminal_fallback_text = "" + if terminal_fallback_targets: + terminal_fallback_text = " 端子退回目标 {0} 个,已存在 {1} 条。".format( + int(terminal_fallback_targets or 0), + int(terminal_fallback_duplicates or 0), + ) missing_text = "" if missing_detour_pairs: missing_text = " 未找到配对:{0}。".format("、".join(missing_detour_pairs[:3])) + if missing_terminal_fallback_refs: + missing_text += " 未找到端子退回目标:{0}。".format( + "、".join(missing_terminal_fallback_refs[:3]) + ) self._set_status( - "未按诊断建议生成桥接。建议 {0} 条,已存在 {1} 条,失效 {2} 条。{3}{4}请先点击“检查布线路径网络”或“汇总布线诊断”确认是否存在可桥接建议。{5}".format( + "未按诊断建议生成桥接。建议 {0} 条,已存在 {1} 条,失效 {2} 条。{3}{4}{5}请先点击“检查布线路径网络”或“汇总布线诊断”确认是否存在可桥接建议。{6}".format( suggestions, duplicates, stale, detour_text, + terminal_fallback_text, missing_text, self.controller.summary(), ) @@ -3035,13 +4031,20 @@ class AutoRoutingTaskPanel: int(detour_pairs or 0), int(detour_created or 0), ) + terminal_fallback_text = "" + if terminal_fallback_targets or terminal_fallback_created: + terminal_fallback_text = " 端子退回目标 {0} 个,生成 {1} 条。".format( + int(terminal_fallback_targets or 0), + int(terminal_fallback_created or 0), + ) self._set_status( - "已按诊断建议生成桥接 UserPath:{0} 条。建议 {1} 条,已存在 {2} 条,失效 {3} 条。{4}{5}".format( + "已按诊断建议生成桥接 UserPath:{0} 条。建议 {1} 条,已存在 {2} 条,失效 {3} 条。{4}{5}请重新生成布线路径网络/布线连接验证效果。{6}".format( created, suggestions, duplicates, stale, detour_text, + terminal_fallback_text, self.controller.summary(), ) ) @@ -3086,6 +4089,25 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def select_wire_outside_boundary_wires(self): + try: + result = self.controller.select_wire_outside_boundary_wires() + selected = result.get("selected_wire_outside_boundary_wires", 0) + missing = list(result.get("missing_wire_outside_boundary_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择越界导线。请先生成布线连接,并确认批量诊断中存在“导线越出柜内区域”。" + + self.controller.summary() + ) + return + message = _format_wire_outside_boundary_selection_status(result) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + message += "请检查这些导线附近是否缺少柜内 UserPath/线槽,或柜内边界是否标记过小。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + def mark_selected_objects_pass_through_obstacle(self): self._mark_selected_objects_obstacle_mode("PassThrough", "忽略碰撞") @@ -3324,6 +4346,112 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def select_terminal_access_fallback_targets(self): + try: + result = self.controller.select_terminal_access_fallback_targets() + wires = result.get("selected_terminal_access_fallback_wires", 0) + targets = result.get("selected_terminal_access_fallback_targets", 0) + missing = list(result.get("missing_terminal_access_fallback_refs", []) or []) + target_names = list(result.get("selected_terminal_access_fallback_target_names", []) or []) + if wires <= 0 and targets <= 0: + self._set_status( + "未选择端子退回位置。请先检查布线路径网络或生成布线连接,确认存在 terminal_access_fallback_targets。" + + self.controller.summary() + ) + return + message = _format_terminal_access_fallback_selection_status(result) + if target_names: + message += " 目标:{0}。".format("、".join(str(name) for name in target_names[:5])) + if missing: + message += " 未找到:{0}。".format("、".join(str(item) for item in missing[:5])) + message += "请优先在这些端子附近补 UserPath、线槽接入桥或设备局部出线路径。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_terminal_access_endpoint_device_avoidance(self): + try: + result = self.controller.select_terminal_access_endpoint_device_avoidance() + terminals = result.get("selected_terminal_access_endpoint_avoidance_terminals", 0) + devices = result.get("selected_terminal_access_endpoint_avoidance_devices", 0) + targets = result.get("selected_terminal_access_endpoint_avoidance_targets", 0) + access_carriers = result.get("selected_terminal_access_endpoint_avoidance_access_carriers", 0) + missing = list(result.get("missing_terminal_access_endpoint_avoidance_refs", []) or []) + if terminals <= 0 and devices <= 0 and targets <= 0 and access_carriers <= 0: + self._set_status( + "未选择端点避让接入对象。请先检查布线路径网络,确认存在 terminal_access_endpoint_device_avoidance。" + + self.controller.summary() + ) + return + message = "已选择端点避让接入:端子 {0} 个,设备 {1} 个,目标路径 {2} 个,接入段 {3} 条。".format( + terminals, + devices, + targets, + access_carriers, + ) + if missing: + message += " 未找到:{0}。".format("、".join(str(item) for item in missing[:5])) + message += "请检查端子局部出线是否贴设备、接入段是否绕回设备包围盒,以及目标主路径入口是否合理。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_unconnected_terminal_access_issues(self): + try: + result = self.controller.select_unconnected_terminal_access_issues() + terminals = result.get("selected_unconnected_terminal_access_terminals", 0) + devices = result.get("selected_unconnected_terminal_access_devices", 0) + max_distance = float(result.get("max_unconnected_terminal_access_distance_mm", 0.0) or 0.0) + missing = list(result.get("missing_unconnected_terminal_access_refs", []) or []) + if terminals <= 0 and devices <= 0: + self._set_status( + "未选择未接入端子。请先检查布线路径网络,确认存在 unconnected_terminals。" + + self.controller.summary() + ) + return + message = "已选择未接入端子:端子 {0} 个,设备 {1} 个;最大最近网络距离 {2:.1f} mm。".format( + terminals, + devices, + max_distance, + ) + if missing: + message += " 未找到:{0}。".format("、".join(str(item) for item in missing[:5])) + message += "请检查设备是否已装配、附近是否缺 UserPath/线槽入口,或端子接入最大距离是否过小。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_terminal_exit_issue_terminals(self): + try: + result = self.controller.select_terminal_exit_issue_terminals() + terminals = result.get("selected_terminal_exit_issue_terminals", 0) + devices = result.get("selected_terminal_exit_issue_devices", 0) + corrected = result.get("selected_terminal_exit_corrected_terminals", 0) + capped = result.get("selected_terminal_exit_capped_terminals", 0) + invalid_direction = result.get("selected_terminal_exit_invalid_direction_terminals", 0) + invalid_local_route = result.get("selected_terminal_exit_invalid_local_route_terminals", 0) + missing = list(result.get("missing_terminal_exit_issue_refs", []) or []) + if terminals <= 0 and devices <= 0: + self._set_status( + "未选择出线问题端子。请先检查布线路径网络,确认存在 corrected_terminal_exits、capped_terminal_exits、invalid_terminal_exit_directions 或 invalid_terminal_local_routes。" + + self.controller.summary() + ) + return + message = "已选择出线问题端子:端子 {0} 个,设备 {1} 个;方向校正 {2} 个,长度截断 {3} 个,显式方向无效 {4} 个,局部路径无效 {5} 个。".format( + terminals, + devices, + corrected, + capped, + invalid_direction, + invalid_local_route, + ) + if missing: + message += " 未找到:{0}。".format("、".join(str(item) for item in missing[:5])) + message += "请优先检查设备模板端子 LCS、显式出线方向和局部出线路径元数据。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + def select_issue_wires(self): try: result = self.controller.select_issue_wires() @@ -3686,7 +4814,7 @@ class AutoRoutingTaskPanel: try: self._sync_options_from_widgets() report = self.controller.route_eplan_connections() - self._set_status(AutoRouting.format_eplan_connection_route_report(report)) + self._set_status(_format_route_panel_status(report)) except Exception as exc: self._set_error(str(exc)) diff --git a/src/Mod/FreeCADExchange/CMakeLists.txt b/src/Mod/FreeCADExchange/CMakeLists.txt index 62207d8..34538df 100644 --- a/src/Mod/FreeCADExchange/CMakeLists.txt +++ b/src/Mod/FreeCADExchange/CMakeLists.txt @@ -5,6 +5,7 @@ set(FreeCADExchange_Scripts ExchangeBootstrap.py DeviceImport.py DevicePreview.py + PendingDeviceAssemblyPanel.py TerminalObjects.py TemplateSemantics.py TemplateAuthoring.py diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index a0e4643..94e2614 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -1,6 +1,7 @@ import os from pathlib import Path import uuid +import json from datetime import datetime import FreeCAD as App @@ -28,6 +29,8 @@ TERMINAL_GROUP_PREFIX = "QETTerminals_" WIRE_GROUP_PREFIX = "QETWires_" GROUP_KIND_TERMINALS = "Terminals" GROUP_KIND_WIRES = "Wires" +ASSEMBLY_STATE_PENDING = "Pending" +ASSEMBLY_STATE_PLACED = "Placed" class DeviceImportError(RuntimeError): @@ -525,18 +528,12 @@ def _device_report_label(display_tag, instance_id, element_uuid=""): def _payload_device_instance_id(device): if not isinstance(device, dict): return "" - return ( - (device.get("device_instance_id") or "").strip() - or (device.get("instance_id") or "").strip() - ) + return (device.get("device_instance_id") or "").strip() def _payload_device_element_uuid(device): if not isinstance(device, dict): return "" - element_uuid = (device.get("element_uuid") or "").strip() - if element_uuid: - return element_uuid for terminal in device.get("terminals", []) or []: if not isinstance(terminal, dict): continue @@ -653,6 +650,16 @@ def _update_device_group_metadata(device_group, root_group, element_uuid, instan ) +def _set_device_assembly_state(device_group, state): + _ensure_string_property( + device_group, + "QetAssemblyState", + "QET Assembly", + "Assembly state in the FreeCAD scene.", + state, + ) + + def _ensure_device_group(doc, root_group, element_uuid, instance_id, model_path, display_tag, layout_index): created_now = False device_group = _find_device_group_by_instance_id(doc, instance_id) @@ -725,6 +732,282 @@ def _ensure_device_group(doc, root_group, element_uuid, instance_id, model_path, return device_group, created_now +def _register_pending_device(report, device_group, display_tag, instance_id, element_uuid, resolved_model_path): + _set_device_assembly_state(device_group, ASSEMBLY_STATE_PENDING) + report.setdefault("pending_devices", 0) + report.setdefault("pending_device_details", []) + report["pending_devices"] += 1 + report["pending_device_details"].append( + _device_change_detail( + display_tag, + instance_id, + element_uuid=element_uuid, + change_types=["待装配"], + resolved_model_path=resolved_model_path, + ) + ) + + +def _looks_like_qet_device_group(obj): + if obj is None: + return False + if not getattr(obj, "Name", "").startswith(DEVICE_GROUP_PREFIX): + return False + return bool((getattr(obj, "QetInstanceId", "") or "").strip()) + + +def _find_device_group_from_object(obj): + if _looks_like_qet_device_group(obj): + return obj + pending = list(getattr(obj, "InList", []) or []) + seen = set() + while pending: + parent = pending.pop(0) + parent_name = getattr(parent, "Name", "") + if parent_name in seen: + continue + seen.add(parent_name) + if _looks_like_qet_device_group(parent): + return parent + pending.extend(list(getattr(parent, "InList", []) or [])) + return None + + +def list_pending_devices(doc): + if doc is None: + return [] + root = doc.getObject(ROOT_GROUP_NAME) + if root is None: + return [] + + pending_devices = [] + for child in list(getattr(root, "Group", []) or []): + if not _looks_like_qet_device_group(child): + continue + if (getattr(child, "QetAssemblyState", "") or "").strip() != ASSEMBLY_STATE_PENDING: + continue + pending_devices.append( + { + "device": child, + "instance_id": (getattr(child, "QetInstanceId", "") or "").strip(), + "element_uuid": (getattr(child, "QetElementUuid", "") or "").strip(), + "display_tag": (getattr(child, "QetDisplayTag", "") or "").strip(), + "label": getattr(child, "Label", "") or getattr(child, "Name", ""), + "resolved_model_path": ( + getattr(child, "QetResolvedModelPath", "") or "" + ).strip(), + } + ) + return pending_devices + + +def _target_mount_kind(target_obj): + if target_obj is None: + return "" + kind = (getattr(target_obj, "QetCarrierKind", "") or "").strip() + if kind: + return kind + text = " ".join( + [ + getattr(target_obj, "Label", "") or "", + getattr(target_obj, "Name", "") or "", + getattr(target_obj, "TypeId", "") or "", + ] + ).lower() + if "rail" in text or "din" in text or "导轨" in text: + return "rail" + if "wireduct" in text or "wire_duct" in text or "线槽" in text: + return "wire_duct" + if "plate" in text or "panel" in text or "安装板" in text or "面板" in text: + return "mounting_plate" + if "cabinet" in text or "柜" in text: + return "cabinet" + return "" + + +def _placement_for_mount_target(mount_target, fallback_rotation=None): + placement = getattr(mount_target, "Placement", None) + base = getattr(placement, "Base", None) + if placement is None or base is None: + return None + rotation = getattr(placement, "Rotation", None) or fallback_rotation or App.Rotation() + return App.Placement(base, rotation) + + +def _vector_payload(vector): + return { + "x": float(getattr(vector, "x", 0.0) or 0.0), + "y": float(getattr(vector, "y", 0.0) or 0.0), + "z": float(getattr(vector, "z", 0.0) or 0.0), + } + + +def _normalized_vector(vector): + if vector is None: + return None + x = float(getattr(vector, "x", 0.0) or 0.0) + y = float(getattr(vector, "y", 0.0) or 0.0) + z = float(getattr(vector, "z", 0.0) or 0.0) + length = (x * x + y * y + z * z) ** 0.5 + if length <= 1e-9: + return None + return App.Vector(x / length, y / length, z / length) + + +def _placement_with_normal_offset(placement, normal=None, offset_mm=0.0): + if placement is None: + return None + normal = _normalized_vector(normal) + if normal is None or not float(offset_mm or 0.0): + return placement + base = getattr(placement, "Base", None) + if base is None: + return placement + offset = float(offset_mm or 0.0) + moved_base = App.Vector( + float(getattr(base, "x", 0.0) or 0.0) + normal.x * offset, + float(getattr(base, "y", 0.0) or 0.0) + normal.y * offset, + float(getattr(base, "z", 0.0) or 0.0) + normal.z * offset, + ) + return App.Placement(moved_base, getattr(placement, "Rotation", App.Rotation())) + + +def _set_device_mount_metadata(device_group, mount_target, normal=None, offset_mm=0.0): + if device_group is None or mount_target is None: + return + target_name = getattr(mount_target, "Name", "") or "" + target_label = getattr(mount_target, "Label", "") or target_name + _ensure_string_property( + device_group, + "QetMountMode", + "QET Mount", + "How this QET device was mounted in the FreeCAD scene.", + "manual_insert", + ) + _ensure_string_property( + device_group, + "QetMountHostName", + "QET Mount", + "Mount target object name.", + target_name, + ) + _ensure_string_property( + device_group, + "QetMountHostLabel", + "QET Mount", + "Mount target object label.", + target_label, + ) + _ensure_string_property( + device_group, + "QetMountHostKind", + "QET Mount", + "Mount target kind.", + _target_mount_kind(mount_target), + ) + normal = _normalized_vector(normal) + if normal is not None: + _ensure_string_property( + device_group, + "QetMountHostNormalJson", + "QET Mount", + "Mount target face normal at insert time.", + json.dumps(_vector_payload(normal), sort_keys=True), + ) + _ensure_string_property( + device_group, + "QetMountOffsetMm", + "QET Mount", + "Mount offset in target normal direction.", + "{0:.6f}".format(float(offset_mm or 0.0)), + ) + + +def insert_pending_device( + doc, + device_group, + source_doc_cache=None, + mount_target=None, + mount_placement=None, + mount_normal=None, + mount_offset_mm=0.0, +): + if doc is None: + raise DeviceImportError("A FreeCAD document is required.") + device_group = _find_device_group_from_object(device_group) + if device_group is None: + raise DeviceImportError("请选择一个待装配 QET 设备。") + + model_path = _native_path(getattr(device_group, "QetResolvedModelPath", "")) + if not model_path: + raise DeviceImportError("待装配设备缺少模型路径。") + if not os.path.isfile(model_path): + raise DeviceImportError("待装配设备模型文件不存在:{0}".format(model_path)) + if not _supported_for_import(model_path): + raise DeviceImportError("待装配设备模型格式暂不支持:{0}".format(model_path)) + + existing_model_objects = _existing_model_objects(doc, device_group) + if existing_model_objects: + _set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED) + target_placement = mount_placement or _placement_for_mount_target( + mount_target, + getattr(getattr(device_group, "Placement", None), "Rotation", None), + ) + target_placement = _placement_with_normal_offset( + target_placement, + mount_normal, + mount_offset_mm, + ) + if target_placement is not None: + device_group.Placement = target_placement + _set_device_mount_metadata( + device_group, + mount_target, + normal=mount_normal, + offset_mm=mount_offset_mm, + ) + return { + "device": device_group, + "imported_objects": existing_model_objects, + "already_placed": True, + } + + _clear_group_contents(doc, device_group) + imported_objects = _import_model_into_group( + doc, + device_group, + model_path, + source_doc_cache=source_doc_cache if source_doc_cache is not None else {}, + ) + target_placement = mount_placement or _placement_for_mount_target( + mount_target, + getattr(getattr(device_group, "Placement", None), "Rotation", None), + ) + target_placement = _placement_with_normal_offset( + target_placement, + mount_normal, + mount_offset_mm, + ) + if target_placement is not None: + device_group.Placement = target_placement + _set_device_mount_metadata( + device_group, + mount_target, + normal=mount_normal, + offset_mm=mount_offset_mm, + ) + _set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED) + try: + doc.recompute() + except Exception: + pass + return { + "device": device_group, + "imported_objects": list(imported_objects or []), + "already_placed": False, + } + + def _remove_object_tree(doc, obj): if obj is None: return @@ -1208,15 +1491,9 @@ def _close_cached_source_documents(source_doc_cache, target_doc=None): def _model_index(payload): index = {} for item in payload.get("device_models", []): - instance_id = ( - (item.get("device_instance_id") or "").strip() - or (item.get("instance_id") or "").strip() - ) - element_uuid = (item.get("element_uuid") or "").strip() + instance_id = (item.get("device_instance_id") or "").strip() if instance_id and instance_id not in index: index[instance_id] = item - if element_uuid and element_uuid not in index: - index[element_uuid] = item return index @@ -1313,7 +1590,7 @@ def _import_cabinet_model(doc, root_group, cabinet, report, source_doc_cache=Non ) -def import_devices_from_payload(payload, scene_path=""): +def import_devices_from_payload(payload, scene_path="", auto_insert_pending_devices=False): _append_debug_log("DeviceImport.import_devices_from_payload entered") doc = _ensure_document(scene_path) cabinet = payload.get("cabinet") @@ -1345,6 +1622,8 @@ def import_devices_from_payload(payload, scene_path=""): "cabinet_skipped_missing_file": 0, "cabinet_skipped_unsupported_format": 0, "cabinet_skipped_import_error": 0, + "pending_devices": 0, + "pending_device_details": [], "warnings": [], } @@ -1390,8 +1669,6 @@ def import_devices_from_payload(payload, scene_path=""): doc, existing_device_group ) model_info = models_by_element.get(instance_id, {}) - if not model_info and element_uuid: - model_info = models_by_element.get(element_uuid, {}) resolved_model_path = _native_path(model_info.get("resolved_model_path", "")) _append_debug_log( "DeviceImport device instance_id={0}, display_tag={1}, resolved_model_path={2}".format( @@ -1490,7 +1767,25 @@ def import_devices_from_payload(payload, scene_path=""): not created_now and (not existing_model_objects or not same_source) ) + if not auto_insert_pending_devices and not existing_model_objects: + _register_pending_device( + report, + device_group, + display_tag, + instance_id, + element_uuid, + resolved_model_path, + ) + _append_debug_log( + "DeviceImport registered pending device without importing model: instance_id={0}, model_path={1}".format( + instance_id, + resolved_model_path, + ) + ) + continue + if existing_model_objects and same_source: + _set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED) if display_tag_changed or terminals_changed: change_types = [] if display_tag_changed: @@ -1562,6 +1857,7 @@ def import_devices_from_payload(payload, scene_path=""): ) if existing_model_objects: _remove_model_objects(doc, existing_model_objects) + _set_device_assembly_state(device_group, ASSEMBLY_STATE_PLACED) except Exception as exc: if existing_model_objects: _ensure_string_property( @@ -1643,3 +1939,66 @@ def import_devices_from_payload(payload, scene_path=""): ) ) return report + + +class CommandInsertPendingDevice: + def GetResources(self): + return { + "MenuText": "插入待装配设备", + "ToolTip": "将选中的 QET 待装配设备模型插入当前 3D 场景", + } + + def IsActive(self): + return getattr(App, "ActiveDocument", None) is not None and Gui is not None + + def Activated(self): + if Gui is None: + return + selection = list(Gui.Selection.getSelection() or []) + device_group = None + for obj in selection: + device_group = _find_device_group_from_object(obj) + if device_group is not None: + break + if device_group is None: + try: + App.Console.PrintWarning("[FreeCADExchange] 请先选择一个待装配 QET 设备。\n") + except Exception: + pass + return + try: + result = insert_pending_device(App.ActiveDocument, device_group) + try: + App.Console.PrintMessage( + "[FreeCADExchange] 已插入设备:{0},导入对象 {1} 个。\n".format( + getattr(result["device"], "Label", ""), + len(result.get("imported_objects", []) or []), + ) + ) + except Exception: + pass + try: + Gui.SendMsgToActiveView("ViewFit") + except Exception: + pass + except Exception as exc: + try: + App.Console.PrintError("[FreeCADExchange] 插入待装配设备失败:{0}\n".format(exc)) + except Exception: + pass + + +_COMMANDS_REGISTERED = False + + +def register_commands(): + global _COMMANDS_REGISTERED + if _COMMANDS_REGISTERED: + return + if Gui is None or not hasattr(Gui, "addCommand"): + return + try: + Gui.addCommand("QET_Exchange_InsertPendingDevice", CommandInsertPendingDevice()) + _COMMANDS_REGISTERED = True + except Exception as exc: + _append_debug_log("failed to register pending device command: {0}".format(exc)) diff --git a/src/Mod/FreeCADExchange/ExchangeBootstrap.py b/src/Mod/FreeCADExchange/ExchangeBootstrap.py index 4a89645..3140582 100644 --- a/src/Mod/FreeCADExchange/ExchangeBootstrap.py +++ b/src/Mod/FreeCADExchange/ExchangeBootstrap.py @@ -567,7 +567,7 @@ def _normalize_devices(payload): normalized_terminals.append( { "terminal_uuid": terminal_uuid, - "instance_id": device_instance_id, + "device_instance_id": device_instance_id, "element_uuid": terminal_element_uuid, "terminal_display": _optional_string( terminal_item, "terminal_display", terminal_entry_label @@ -591,7 +591,7 @@ def _normalize_devices(payload): { "element_uuid": element_uuid, "element_uuids": list(device_element_uuids), - "instance_id": device_instance_id, + "device_instance_id": device_instance_id, "display_tag": display_tag.strip() if isinstance(display_tag, str) else "", "terminals": normalized_terminals, } @@ -604,8 +604,8 @@ def _normalize_terminals(devices): for device in devices: for terminal in device.get("terminals", []) or []: entry = dict(terminal) - if not entry.get("instance_id"): - entry["instance_id"] = device.get("instance_id", "") + if not entry.get("device_instance_id"): + entry["device_instance_id"] = device.get("device_instance_id", "") normalized.append(entry) return normalized @@ -618,23 +618,6 @@ def _normalize_top_level_terminals(payload): return [] -def _merge_terminal_entries(*terminal_groups): - merged = [] - seen = set() - for terminal_group in terminal_groups: - for item in terminal_group: - key = ( - item.get("terminal_uuid", ""), - item.get("element_uuid", ""), - item.get("instance_id", ""), - ) - if key in seen: - continue - seen.add(key) - merged.append(item) - return merged - - def _optional_string(item, field_name, entry_label): value = item.get(field_name, "") if value is None: @@ -710,10 +693,8 @@ def _normalize_wires(payload): "wire_mark_is_manual": wire_mark_is_manual, "wire_style_id": _optional_text(item, "wire_style_id"), "start_element_uuid": _optional_string(item, "start_element_uuid", entry_label), - "start_instance_id": _optional_string(item, "start_instance_id", entry_label), "start_terminal_uuid": _optional_string(item, "start_terminal_uuid", entry_label), "end_element_uuid": _optional_string(item, "end_element_uuid", entry_label), - "end_instance_id": _optional_string(item, "end_instance_id", entry_label), "end_terminal_uuid": _optional_string(item, "end_terminal_uuid", entry_label), "start_terminal_display": _optional_string(item, "start_terminal_display", entry_label), "end_terminal_display": _optional_string(item, "end_terminal_display", entry_label), @@ -747,7 +728,6 @@ def _normalize_device_models(payload): entry_label ) ) - element_uuid = "" instance_id = _require_string(item, "device_instance_id") parts_3d = item.get("parts_3d", "") if parts_3d and not isinstance(parts_3d, str): @@ -774,8 +754,7 @@ def _normalize_device_models(payload): normalized.append( { - "element_uuid": element_uuid, - "instance_id": instance_id, + "device_instance_id": instance_id, "device_id": device_id, "parts_3d": parts_3d.strip() if isinstance(parts_3d, str) else "", "resolved_model_path": ( @@ -906,16 +885,17 @@ def load_exchange_payload(json_path): raise ExchangeValidationError("Exchange JSON root must be an object.") project_uuid = _require_string(payload, "project_uuid") - schema_version = payload.get("schema_version", "1.0") + schema_version = payload.get("schema_version", "") if not isinstance(schema_version, str) or not schema_version.strip(): raise ExchangeValidationError("Field 'schema_version' must be a string.") + if schema_version.strip() != "2.0": + raise ExchangeValidationError( + "Field 'schema_version' must be '2.0' for the 2D/3D exchange v2 protocol." + ) normalized_devices = _normalize_devices(payload) - normalized_terminals = _merge_terminal_entries( - _normalize_terminals(normalized_devices), - _normalize_top_level_terminals(payload), - ) + _normalize_top_level_terminals(payload) normalized = { "schema_version": schema_version.strip(), @@ -924,7 +904,6 @@ def load_exchange_payload(json_path): "source": payload.get("source", {}), "cabinet": _normalize_cabinet(payload), "devices": normalized_devices, - "terminals": normalized_terminals, "device_models": _normalize_device_models(payload), "wires": _normalize_wires(payload), } @@ -936,13 +915,13 @@ def load_exchange_payload(json_path): def _build_summary(payload, json_path): devices = payload["devices"] - terminals = payload["terminals"] + terminals = _normalize_terminals(devices) device_models = payload["device_models"] wires = payload.get("wires", []) cabinet = payload.get("cabinet") - missing_device_instances = sum(1 for item in devices if not item["instance_id"]) + missing_device_instances = sum(1 for item in devices if not item["device_instance_id"]) missing_terminal_instances = sum( - 1 for item in terminals if not item["instance_id"] + 1 for item in terminals if not item["device_instance_id"] ) with_model_paths = sum( 1 for item in device_models if item["resolved_model_path"] or item["parts_3d"] @@ -1073,8 +1052,8 @@ def _mark_stale_objects(payload): # Log each payload device element_uuid for comparison for item in (payload.get("devices", []) or [])[:10]: _append_debug_log( - " payload device: element_uuid={0}, instance_id={1}".format( - item.get("element_uuid", ""), item.get("instance_id", "") + " payload device: element_uuid={0}, device_instance_id={1}".format( + item.get("element_uuid", ""), item.get("device_instance_id", "") ) ) diff --git a/src/Mod/FreeCADExchange/InitGui.py b/src/Mod/FreeCADExchange/InitGui.py index 0173a0b..d2ced15 100644 --- a/src/Mod/FreeCADExchange/InitGui.py +++ b/src/Mod/FreeCADExchange/InitGui.py @@ -12,6 +12,8 @@ COMMANDS = [ "QET_Template_AddTerminal", "QET_Template_ValidateTerminals", "QET_Template_SaveAsFCStd", + "QET_Exchange_OpenPendingDevicePanel", + "QET_Exchange_InsertPendingDevice", "QET_Template_ImportInstance", "QET_Template_CreateEngineeringTerminals", "QET_Exchange_CreateManualWire", @@ -72,6 +74,8 @@ def _register_exchange_commands( auto_routing_panel = safe_import("AutoRoutingPanel") manual_wiring = safe_import("ManualWiring") manual_wiring_panel = safe_import("ManualWiringPanel") + device_import = safe_import("DeviceImport") + pending_device_panel = safe_import("PendingDeviceAssemblyPanel") stale_object_actions = safe_import("StaleObjectActions") template_authoring = safe_import("TemplateAuthoring") template_authoring_panel = safe_import("TemplateAuthoringPanel") @@ -117,6 +121,26 @@ def _register_exchange_commands( ) ) + try: + if pending_device_panel is not None: + pending_device_panel.register_commands() + except Exception: + append_init_log( + "InitGui failed to register pending device panel command:\n{0}".format( + traceback_module.format_exc() + ) + ) + + try: + if device_import is not None: + device_import.register_commands() + except Exception: + append_init_log( + "InitGui failed to register pending device command:\n{0}".format( + traceback_module.format_exc() + ) + ) + try: if manual_wiring is not None: manual_wiring.register_commands() diff --git a/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py b/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py new file mode 100644 index 0000000..f09a305 --- /dev/null +++ b/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py @@ -0,0 +1,309 @@ +# FreeCADExchange GUI panel for QET pending device assembly. + +from pathlib import Path + +import FreeCAD as App + +try: + import FreeCADGui as Gui +except ImportError: + Gui = None + +try: + from PySide6 import QtCore, QtWidgets +except ImportError: + try: + from PySide2 import QtCore, QtWidgets + except ImportError: + try: + from PySide import QtCore + from PySide import QtGui as QtWidgets + except ImportError: + QtCore = None + QtWidgets = None + +import DeviceImport + + +COMMAND_NAME = "QET_Exchange_OpenPendingDevicePanel" + + +class PendingDeviceAssemblyPanelError(RuntimeError): + pass + + +def pending_device_rows(doc): + rows = [] + for item in DeviceImport.list_pending_devices(doc): + model_path = (item.get("resolved_model_path", "") or "").strip() + model_name = Path(model_path).name if model_path else "未绑定模型" + display_tag = (item.get("display_tag", "") or "").strip() + label = display_tag or (item.get("label", "") or "").strip() + instance_id = (item.get("instance_id", "") or "").strip() + title = label or instance_id or "未命名设备" + rows.append( + { + "device": item.get("device"), + "display_tag": display_tag, + "instance_id": instance_id, + "element_uuid": (item.get("element_uuid", "") or "").strip(), + "resolved_model_path": model_path, + "display_text": "{0} {1}".format(title, model_name), + } + ) + return rows + + +def _document(): + doc = getattr(App, "ActiveDocument", None) + if doc is None: + raise PendingDeviceAssemblyPanelError("请先打开 FreeCAD 工程。") + return doc + + +def _selected_objects(): + if Gui is None: + return [] + try: + return list(Gui.Selection.getSelection() or []) + except Exception: + return [] + + +def selected_mount_target(exclude_device=None): + excluded = DeviceImport._find_device_group_from_object(exclude_device) + for obj in _selected_objects(): + candidate_device = DeviceImport._find_device_group_from_object(obj) + if excluded is not None and candidate_device is excluded: + continue + return obj + return None + + +def _vector_from_tuple(value): + try: + if value is None: + return None + return App.Vector(value[0], value[1], value[2]) + except Exception: + return None + + +def _face_anchor_point(face): + if face is None: + return None + for attr_name in ("CenterOfMass", "Center"): + point = getattr(face, attr_name, None) + if point is not None: + return point + try: + return face.valueAt(0.0, 0.0) + except Exception: + return None + + +def _face_normal(face): + if face is None: + return None + for attr_name in ("normalAt", "NormalAt"): + normal_getter = getattr(face, attr_name, None) + if callable(normal_getter): + try: + return normal_getter(0.0, 0.0) + except Exception: + pass + return getattr(face, "Normal", None) + + +def selected_mount_context(exclude_device=None): + excluded = DeviceImport._find_device_group_from_object(exclude_device) + if Gui is not None: + try: + selection_ex = list(Gui.Selection.getSelectionEx() or []) + except Exception: + selection_ex = [] + for selected in selection_ex: + obj = getattr(selected, "Object", None) + candidate_device = DeviceImport._find_device_group_from_object(obj) + if excluded is not None and candidate_device is excluded: + continue + picked_points = list(getattr(selected, "PickedPoints", []) or []) + point = picked_points[0] if picked_points else None + if point is None: + for sub_object in list(getattr(selected, "SubObjects", []) or []): + if (getattr(sub_object, "ShapeType", "") or "").lower() == "face": + point = _face_anchor_point(sub_object) + normal = _face_normal(sub_object) + break + else: + normal = None + for sub_object in list(getattr(selected, "SubObjects", []) or []): + if (getattr(sub_object, "ShapeType", "") or "").lower() == "face": + normal = _face_normal(sub_object) + break + if point is not None: + rotation = getattr(getattr(obj, "Placement", None), "Rotation", None) + return { + "target": obj, + "placement": App.Placement(point, rotation or App.Rotation()), + "normal": normal, + } + if obj is not None: + return {"target": obj, "placement": None, "normal": None} + + target = selected_mount_target(exclude_device=exclude_device) + return {"target": target, "placement": None, "normal": None} + + +def _set_status(label, message, error=False): + try: + label.setText(message) + except Exception: + pass + try: + if error: + App.Console.PrintError("[FreeCADExchange] {0}\n".format(message)) + else: + App.Console.PrintMessage("[FreeCADExchange] {0}\n".format(message)) + except Exception: + pass + + +class PendingDeviceAssemblyTaskPanel: + def __init__(self): + if QtWidgets is None: + raise PendingDeviceAssemblyPanelError("Qt widgets are not available.") + + self.rows = [] + self.form = QtWidgets.QWidget() + self.form.setWindowTitle("QET待装配设备") + + layout = QtWidgets.QVBoxLayout(self.form) + self.device_list = QtWidgets.QListWidget() + layout.addWidget(self.device_list) + + self.refresh_button = QtWidgets.QPushButton("刷新清单") + self.insert_button = QtWidgets.QPushButton("插入设备") + self.insert_to_target_button = QtWidgets.QPushButton("插入到选中目标") + layout.addWidget(self.refresh_button) + layout.addWidget(self.insert_button) + + offset_row = QtWidgets.QHBoxLayout() + offset_row.addWidget(QtWidgets.QLabel("贴合间距")) + self.mount_offset_input = QtWidgets.QDoubleSpinBox() + self.mount_offset_input.setRange(-10000.0, 10000.0) + self.mount_offset_input.setDecimals(1) + self.mount_offset_input.setSingleStep(1.0) + self.mount_offset_input.setSuffix(" mm") + self.mount_offset_input.setValue(0.0) + offset_row.addWidget(self.mount_offset_input) + layout.addLayout(offset_row) + + layout.addWidget(self.insert_to_target_button) + + self.status_label = QtWidgets.QLabel("") + self.status_label.setWordWrap(True) + layout.addWidget(self.status_label) + + self.refresh_button.clicked.connect(self.refresh) + self.insert_button.clicked.connect(self.insert_selected_device) + self.insert_to_target_button.clicked.connect(self.insert_selected_device_to_target) + + self.refresh() + + def _selected_device(self): + item = self.device_list.currentItem() + if item is None: + raise PendingDeviceAssemblyPanelError("请先在清单中选择一个待装配设备。") + row_index = self.device_list.row(item) + if row_index < 0 or row_index >= len(self.rows): + raise PendingDeviceAssemblyPanelError("待装配设备清单已变化,请刷新后重试。") + device = self.rows[row_index].get("device") + if device is None: + raise PendingDeviceAssemblyPanelError("待装配设备无效,请刷新后重试。") + return device + + def refresh(self): + try: + self.rows = pending_device_rows(getattr(App, "ActiveDocument", None)) + self.device_list.clear() + for row in self.rows: + self.device_list.addItem(row["display_text"]) + if self.rows: + self.device_list.setCurrentRow(0) + _set_status(self.status_label, "待装配设备:{0} 个".format(len(self.rows))) + except Exception as exc: + _set_status(self.status_label, str(exc), error=True) + + def insert_selected_device(self): + try: + result = DeviceImport.insert_pending_device(_document(), self._selected_device()) + self.refresh() + _set_status( + self.status_label, + "已插入设备:{0}".format(getattr(result["device"], "Label", "") or getattr(result["device"], "Name", "")), + ) + except Exception as exc: + _set_status(self.status_label, str(exc), error=True) + + def insert_selected_device_to_target(self): + try: + device = self._selected_device() + context = selected_mount_context(exclude_device=device) + target = context.get("target") + if target is None: + raise PendingDeviceAssemblyPanelError("请先在 3D 视图中选择安装板、导轨、线槽或柜体安装面。") + result = DeviceImport.insert_pending_device( + _document(), + device, + mount_target=target, + mount_placement=context.get("placement"), + mount_normal=context.get("normal"), + mount_offset_mm=self.mount_offset_input.value(), + ) + self.refresh() + _set_status( + self.status_label, + "已插入设备到选中目标:{0}".format( + getattr(result["device"], "Label", "") or getattr(result["device"], "Name", "") + ), + ) + except Exception as exc: + _set_status(self.status_label, str(exc), error=True) + + def accept(self): + return True + + def reject(self): + return True + + +class CommandOpenPendingDevicePanel: + def GetResources(self): + return { + "MenuText": "待装配设备", + "ToolTip": "打开 QET 待装配设备清单,并将设备插入到当前 3D 场景", + } + + def IsActive(self): + return getattr(App, "ActiveDocument", None) is not None and Gui is not None + + def Activated(self): + if Gui is None or not hasattr(Gui, "Control"): + return + if hasattr(Gui.Control, "activeDialog") and Gui.Control.activeDialog(): + Gui.Control.closeDialog() + Gui.Control.showDialog(PendingDeviceAssemblyTaskPanel()) + + +_COMMANDS_REGISTERED = False + + +def register_commands(): + global _COMMANDS_REGISTERED + if _COMMANDS_REGISTERED: + return + if Gui is None or not hasattr(Gui, "addCommand"): + return + Gui.addCommand(COMMAND_NAME, CommandOpenPendingDevicePanel()) + _COMMANDS_REGISTERED = True diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 126ab24..ea9c8a4 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -58,6 +58,7 @@ DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY = 1000.0 DEFAULT_TERMINAL_ACCESS_FALLBACK_CARRIER_PENALTY = 5000.0 DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY = 2000.0 DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE = 10.0 +DEFAULT_TERMINAL_EXIT_MAX_LENGTH = 80.0 DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0 DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 WIRE_DUCT_OBSTACLE_MODE = "PassThrough" @@ -1119,6 +1120,23 @@ def _segment_hits_blocked_bbox(start, end, blocked_bboxes): return False +def _bbox_payload(bbox, clearance=0.0): + if bbox is None: + return None + margin = max(float(clearance or 0.0), 0.0) + try: + return { + "xmin": float(bbox.XMin) - margin, + "xmax": float(bbox.XMax) + margin, + "ymin": float(bbox.YMin) - margin, + "ymax": float(bbox.YMax) + margin, + "zmin": float(bbox.ZMin) - margin, + "zmax": float(bbox.ZMax) + margin, + } + except Exception: + return None + + def collect_route_carriers(doc): if doc is None: return [] @@ -2304,8 +2322,8 @@ def _route_carriers_for_bridge_object(doc, source): return carriers -def create_user_path_bridge_between_objects(doc, left_source, right_source, project_uuid=""): - """Create a UserPath bridge between the nearest carriers of two selected source objects.""" +def nearest_route_bridge_candidate_between_objects(doc, left_source, right_source): + """Return the nearest bridge candidate between two route sources/carriers.""" left_carriers = _route_carriers_for_bridge_object(doc, left_source) right_carriers = _route_carriers_for_bridge_object(doc, right_source) best = None @@ -2319,12 +2337,33 @@ def create_user_path_bridge_between_objects(doc, left_source, right_source, proj if nearest is None: continue distance_mm, left_point, right_point = nearest - if best is None or float(distance_mm) < float(best[0]): - best = (distance_mm, left, right, left_point, right_point) + if best is None or float(distance_mm) < float(best["distance_mm"]): + best = { + "distance_mm": float(distance_mm), + "left_carrier": left, + "right_carrier": right, + "left_point": left_point, + "right_point": right_point, + } + return best + + +def create_user_path_bridge_between_objects( + doc, + left_source, + right_source, + project_uuid="", + bridge_kind="MainPathDetourBridge", +): + """Create a UserPath bridge between the nearest carriers of two selected source objects.""" + best = nearest_route_bridge_candidate_between_objects(doc, left_source, right_source) if best is None: return [] - distance_mm, left, right, left_point, right_point = best + left = best["left_carrier"] + right = best["right_carrier"] + left_point = best["left_point"] + right_point = best["right_point"] if _distance(left_point, right_point) <= DEFAULT_NODE_TOLERANCE: return [] if _route_bridge_already_exists(doc, left_point, right_point): @@ -2363,7 +2402,7 @@ def create_user_path_bridge_between_objects(doc, left_source, right_source, proj "QetRouteBridgeKind", PROPERTY_GROUP, "QET route bridge kind", - "MainPathDetourBridge", + str(bridge_kind or "MainPathDetourBridge"), ) TerminalObjects.ensure_string_property( bridge, @@ -3463,6 +3502,93 @@ def _terminal_local_route_issue(terminal): return payload +def _terminal_exit_direction_issue(terminal): + invalid_samples = [] + saw_raw = False + for property_name in ("QetTerminalExitDirectionJson", "QetExitDirectionJson"): + raw = (getattr(terminal, property_name, "") or "").strip() + if not raw: + continue + saw_raw = True + parsed = None + try: + parsed = json.loads(raw) + except Exception as exc: + parts = [part.strip() for part in raw.replace(";", ",").split(",")] + if len(parts) < 3: + invalid_samples.append( + { + "property_name": property_name, + "reason": "invalid_json", + "message": str(exc), + "raw_sample": raw[:160], + } + ) + continue + parsed = parts[:3] + + direction = None + if isinstance(parsed, dict): + try: + direction = App.Vector( + float(parsed.get("x", 0.0) or 0.0), + float(parsed.get("y", 0.0) or 0.0), + float(parsed.get("z", 0.0) or 0.0), + ) + except Exception as exc: + invalid_samples.append( + { + "property_name": property_name, + "reason": "invalid_vector", + "message": str(exc), + "raw_sample": raw[:160], + } + ) + continue + elif isinstance(parsed, (list, tuple)) and len(parsed) >= 3: + try: + direction = App.Vector(float(parsed[0] or 0.0), float(parsed[1] or 0.0), float(parsed[2] or 0.0)) + except Exception as exc: + invalid_samples.append( + { + "property_name": property_name, + "reason": "invalid_vector", + "message": str(exc), + "raw_sample": raw[:160], + } + ) + continue + else: + invalid_samples.append( + { + "property_name": property_name, + "reason": "unsupported_shape", + "message": "Exit direction must be a vector object, array, or comma-separated x,y,z text.", + "raw_sample": raw[:160], + } + ) + continue + + normalized = _normalize(direction) + if normalized is not None: + return None + invalid_samples.append( + { + "property_name": property_name, + "reason": "zero_vector", + "message": "Exit direction vector length must be greater than 0.", + "raw_sample": raw[:160], + } + ) + if not saw_raw or not invalid_samples: + return None + payload = _terminal_diagnostic_payload(terminal) + payload.update(invalid_samples[0]) + payload["invalid_samples"] = invalid_samples + payload["code"] = "terminal_exit_direction_invalid" + return payload + + def _terminal_parent_chain(terminal): chain = [] current = terminal @@ -3531,19 +3657,207 @@ def _ray_exit_distance_from_bbox(origin, direction, bbox): return min(distances) -def _terminal_device_aware_exit_point(terminal, exit_length): +def _terminal_exit_direction_candidates(preferred_direction): + preferred = _normalize(_vector(preferred_direction)) + candidates = [] + + def add_candidate(direction): + normalized = _normalize(_vector(direction)) + if normalized is None: + return + key = ( + round(float(normalized.x), 6), + round(float(normalized.y), 6), + round(float(normalized.z), 6), + ) + if key in [item[0] for item in candidates]: + return + candidates.append((key, normalized)) + + add_candidate(preferred or App.Vector(0, 0, 1)) + for direction in ( + App.Vector(1, 0, 0), + App.Vector(-1, 0, 0), + App.Vector(0, 1, 0), + App.Vector(0, -1, 0), + App.Vector(0, 0, 1), + App.Vector(0, 0, -1), + ): + add_candidate(direction) + if preferred is not None: + # 反向通常意味着从设备背面/底面退出,只有其它轴向没有更好出口时才采用。 + add_candidate(_scale(preferred, -1.0)) + return [direction for _key, direction in candidates] + + +def _terminal_exit_required_length(origin, direction, bbox): + exit_distance = _ray_exit_distance_from_bbox(origin, direction, bbox) + if exit_distance is None: + return None + return exit_distance + DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE + + +def _correct_default_terminal_exit_direction(origin, direction, bbox, max_length): + if bbox is None or max_length <= 0.0: + return None + current_required = _terminal_exit_required_length(origin, direction, bbox) + if current_required is None or current_required <= max_length + DEFAULT_NODE_TOLERANCE: + return None + + ranked = [] + for candidate in _terminal_exit_direction_candidates(direction): + required = _terminal_exit_required_length(origin, candidate, bbox) + if required is None: + continue + ranked.append((float(required), candidate)) + if not ranked: + return None + ranked.sort(key=lambda item: item[0]) + best_required, best_direction = ranked[0] + if best_required + DEFAULT_NODE_TOLERANCE >= current_required: + return None + return { + "direction": best_direction, + "device_exit_required_length_mm": float(best_required), + "original_device_exit_required_length_mm": float(current_required), + } + + +def _terminal_exit_plan(terminal, exit_length=20.0, max_exit_length=None): origin = _vector(TerminalObjects.terminal_origin(terminal)) direction = _normalize(_vector(TerminalObjects.terminal_direction(terminal))) if direction is None: direction = App.Vector(0, 0, 1) + original_direction = direction + try: + direction_source = TerminalObjects.terminal_direction_source(terminal) + except Exception: + direction_source = "lcs" - length = max(float(exit_length or 0.0), 0.0) + requested_length = max(float(exit_length or 0.0), 0.0) + max_length = DEFAULT_TERMINAL_EXIT_MAX_LENGTH if max_exit_length is None else max(float(max_exit_length or 0.0), 0.0) + length = requested_length + required_length = 0.0 bbox = _terminal_parent_device_bbox(terminal, origin) - exit_distance = _ray_exit_distance_from_bbox(origin, direction, bbox) - if exit_distance is not None: + corrected = False + original_required_length = 0.0 + if direction_source != "explicit": + correction = _correct_default_terminal_exit_direction(origin, direction, bbox, max_length) + if correction is not None: + # 默认 LCS 方向如果要穿过很深的设备包围盒,优先改用最近的出线面; + # 显式方向不自动改,留给设备模板或人工 CPoint 数据负责。 + direction = correction["direction"] + required_length = float(correction["device_exit_required_length_mm"]) + original_required_length = float(correction["original_device_exit_required_length_mm"]) + corrected = True + if not corrected: + exit_distance = _ray_exit_distance_from_bbox(origin, direction, bbox) + if exit_distance is not None: + required_length = exit_distance + DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE + if required_length > 0.0: # 没有人工局部路径时,默认出线至少先离开所属设备外轮廓,避免导线贴在模型内部。 - length = max(length, exit_distance + DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE) - return _add(origin, _scale(direction, length)) + length = max(length, required_length) + capped = False + if max_length > 0.0 and length > max_length: + # 工程上不能为了离开一个过大的包围盒无限拉长端子出线;超限时截断并交给诊断提示。 + length = max_length + capped = True + return { + "origin": origin, + "direction": direction, + "requested_exit_length_mm": float(requested_length), + "actual_exit_length_mm": float(length), + "max_exit_length_mm": float(max_length), + "device_exit_required_length_mm": float(required_length), + "original_device_exit_required_length_mm": float(original_required_length), + "exit_length_capped": capped, + "exit_direction_source": direction_source, + "exit_direction_corrected": corrected, + "original_direction": original_direction, + "point": _add(origin, _scale(direction, length)), + "device_bbox_detected": required_length > 0.0, + } + + +def _terminal_device_aware_exit_point(terminal, exit_length, max_exit_length=None): + return _terminal_exit_plan( + terminal, + exit_length=exit_length, + max_exit_length=max_exit_length, + )["point"] + + +def terminal_access_diagnostics(terminal, exit_length=20.0, max_exit_length=None): + """Return engineering diagnostics for the terminal's first exit segment.""" + local_points = _terminal_local_route_points(terminal) + if local_points: + origin = _vector(TerminalObjects.terminal_origin(terminal)) + points = [_terminal_local_point_to_global(terminal, point) for point in local_points] + if not points or _distance(points[0], origin) > DEFAULT_NODE_TOLERANCE: + points.insert(0, origin) + points = _normalized_route_points(points) + if len(points) >= 2: + direction = _normalize(_subtract(points[1], points[0])) or App.Vector(0, 0, 1) + return { + "requested_exit_length_mm": max(float(exit_length or 0.0), 0.0), + "actual_exit_length_mm": float(sum(_distance(points[index], points[index + 1]) for index in range(len(points) - 1))), + "max_exit_length_mm": DEFAULT_TERMINAL_EXIT_MAX_LENGTH if max_exit_length is None else max(float(max_exit_length or 0.0), 0.0), + "device_exit_required_length_mm": 0.0, + "original_device_exit_required_length_mm": 0.0, + "exit_length_capped": False, + "exit_direction_source": "local_route", + "exit_direction_corrected": False, + "exit_rule": "local_route", + "local_route_used": True, + "local_route_point_count": len(points), + "device_bbox_detected": False, + "exit_direction": { + "x": round(float(direction.x), 6), + "y": round(float(direction.y), 6), + "z": round(float(direction.z), 6), + }, + "original_exit_direction": { + "x": round(float(direction.x), 6), + "y": round(float(direction.y), 6), + "z": round(float(direction.z), 6), + }, + "origin": _point_payload(points[0]), + "exit_point": _point_payload(points[-1]), + } + + plan = _terminal_exit_plan( + terminal, + exit_length=exit_length, + max_exit_length=max_exit_length, + ) + direction = plan["direction"] + original_direction = plan["original_direction"] + return { + "requested_exit_length_mm": plan["requested_exit_length_mm"], + "actual_exit_length_mm": plan["actual_exit_length_mm"], + "max_exit_length_mm": plan["max_exit_length_mm"], + "device_exit_required_length_mm": plan["device_exit_required_length_mm"], + "original_device_exit_required_length_mm": plan["original_device_exit_required_length_mm"], + "exit_length_capped": bool(plan["exit_length_capped"]), + "exit_direction_source": plan["exit_direction_source"], + "exit_direction_corrected": bool(plan["exit_direction_corrected"]), + "exit_rule": "default_exit", + "local_route_used": False, + "local_route_point_count": 0, + "device_bbox_detected": bool(plan["device_bbox_detected"]), + "exit_direction": { + "x": round(float(direction.x), 6), + "y": round(float(direction.y), 6), + "z": round(float(direction.z), 6), + }, + "original_exit_direction": { + "x": round(float(original_direction.x), 6), + "y": round(float(original_direction.y), 6), + "z": round(float(original_direction.z), 6), + }, + "origin": _point_payload(plan["origin"]), + "exit_point": _point_payload(plan["point"]), + } def _terminal_local_point_to_global(terminal, local_point): @@ -3617,6 +3931,65 @@ def set_terminal_local_route_points(terminal, document_points): } +def set_terminal_exit_direction(terminal, document_points): + """Store an explicit document-space CPoint direction on one engineering terminal.""" + if not TerminalObjects.is_terminal_object(terminal): + raise RoutingNetworkError("请选择一个可布线端子,再设置端子出线方向。") + points = _normalized_route_points([_vector(point) for point in list(document_points or [])]) + if len(points) < 2: + raise RoutingNetworkError("端子出线方向至少需要两个有效点。") + direction = _normalize(_subtract(points[1], points[0])) + if direction is None: + raise RoutingNetworkError("端子出线方向长度为 0,请选择一条有效线段或两个不同点。") + payload = { + "x": round(float(direction.x), 6), + "y": round(float(direction.y), 6), + "z": round(float(direction.z), 6), + } + TerminalObjects.ensure_string_property( + terminal, + "QetTerminalExitDirectionJson", + PROPERTY_GROUP, + "端子显式出线方向,使用 FreeCAD 文档坐标", + json.dumps(payload, ensure_ascii=False), + ) + try: + terminal.Document.recompute() + except Exception: + pass + return { + "terminal": terminal, + "property_name": "QetTerminalExitDirectionJson", + "direction": payload, + "point_count": len(points), + } + + +def set_terminal_exit_direction_from_selection(selection_ex): + """Use one selected terminal and one selected line/path as its explicit exit direction.""" + terminal = None + direction_runs = [] + for item in list(selection_ex or []): + source = getattr(item, "Object", None) + if TerminalObjects.is_terminal_object(source): + if terminal is not None and source is not terminal: + raise RoutingNetworkError("一次只能为一个端子设置出线方向。") + terminal = source + continue + for points in _point_runs_from_selection_item(item): + normalized = _normalize_point_run(points) + if len(normalized) >= 2: + direction_runs.append(normalized) + + if terminal is None: + raise RoutingNetworkError("请同时选中一个可布线端子和一条表示方向的草图/Draft 线或边。") + if not direction_runs: + raise RoutingNetworkError("请选择至少包含两个点的草图、Draft 线、边或路径对象作为端子出线方向。") + if len(direction_runs) > 1: + raise RoutingNetworkError("端子出线方向一次只支持一条方向线,请只选择一条草图线或一个连续 Wire。") + return set_terminal_exit_direction(terminal, direction_runs[0]) + + def set_terminal_local_route_points_from_selection(selection_ex): """Use one selected terminal and one selected sketch/edge path as its local exit path.""" terminal = None @@ -3651,7 +4024,7 @@ def set_terminal_local_route_points_from_selection(selection_ex): return set_terminal_local_route_points(terminal, route_runs[0]) -def terminal_access_path_points(terminal, exit_length=20.0): +def terminal_access_path_points(terminal, exit_length=20.0, max_exit_length=None): """Return terminal-to-network access points, honoring optional local route metadata.""" origin = _vector(TerminalObjects.terminal_origin(terminal)) local_points = _terminal_local_route_points(terminal) @@ -3662,7 +4035,46 @@ def terminal_access_path_points(terminal, exit_length=20.0): normalized = _normalized_route_points(points) if len(normalized) >= 2: return normalized - return _normalized_route_points([origin, _terminal_device_aware_exit_point(terminal, exit_length)]) + return _normalized_route_points( + [origin, _terminal_device_aware_exit_point(terminal, exit_length, max_exit_length=max_exit_length)] + ) + + +def terminal_access_carrier_for_terminal(terminal): + doc = getattr(terminal, "Document", None) + carrier = _live_source_carrier(doc, terminal) + if ( + carrier is not None + and (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() + == ROUTE_CARRIER_KIND_TERMINAL_ACCESS + ): + return carrier + return None + + +def terminal_access_path_points_with_network_access(terminal, exit_length=20.0, max_exit_length=None): + """Return terminal local exit plus its generated TerminalAccess carrier. + + TerminalAccess 是端子自己的短接入路径。布线结果应沿这段接入线进入 + 线槽/UserPath 主网络,但它仍不能作为其它导线共享的主路径。 + """ + points = list( + terminal_access_path_points( + terminal, + exit_length, + max_exit_length=max_exit_length, + ) + ) + carrier = terminal_access_carrier_for_terminal(terminal) + if carrier is None: + return points + carrier_points = _normalized_route_points(getattr(carrier, "Points", []) or []) + if len(carrier_points) < 2: + return points + for point in carrier_points: + if not points or _distance(points[-1], point) > DEFAULT_NODE_TOLERANCE: + points.append(point) + return _normalized_route_points(points) def _orthogonal_access_points(start, end): @@ -3687,6 +4099,94 @@ def _orthogonal_access_points(start, end): return points +def _route_points_hit_bbox(points, bbox_payload): + if not bbox_payload: + return False + normalized = _normalized_route_points(points) + for index in range(max(len(normalized) - 1, 0)): + if _segment_intersects_bbox_payload(normalized[index], normalized[index + 1], bbox_payload): + return True + return False + + +def _orthogonal_access_point_candidates(start, end): + start = _vector(start) + end = _vector(end) + candidates = [_orthogonal_access_points(start, end)] + for axes in ( + ("x", "y", "z"), + ("x", "z", "y"), + ("y", "x", "z"), + ("y", "z", "x"), + ("z", "x", "y"), + ("z", "y", "x"), + ): + points = [start] + current = start + for axis in axes: + if abs(_axis_value(end, axis) - _axis_value(current, axis)) <= DEFAULT_NODE_TOLERANCE: + continue + current = _set_axis(current, axis, _axis_value(end, axis)) + points.append(current) + if _distance(points[-1], end) > DEFAULT_NODE_TOLERANCE: + points.append(end) + candidates.append(points) + return [_normalized_route_points(points) for points in candidates] + + +def _terminal_access_dogleg_candidates(start, end, bbox, clearance): + start = _vector(start) + end = _vector(end) + candidates = [] + for axis in ("x", "y", "z"): + low, high = _bbox_axis_range(bbox, axis) + for via_value in ( + float(low) - float(clearance), + float(high) + float(clearance), + ): + first = _set_axis(start, axis, via_value) + second = _set_axis(end, axis, via_value) + candidates.append(_normalized_route_points([start, first, second, end])) + return candidates + + +def _route_length(points): + total = 0.0 + normalized = _normalized_route_points(points) + for index in range(max(len(normalized) - 1, 0)): + total += _distance(normalized[index], normalized[index + 1]) + return total + + +def _terminal_access_points_to_target(exit_point, target_point, endpoint_bbox=None): + default_points = _orthogonal_access_points(exit_point, target_point) + if endpoint_bbox is None: + return default_points, False + start = _vector(exit_point) + end = _vector(target_point) + if _point_inside_bbox(start, endpoint_bbox) or _point_inside_bbox(end, endpoint_bbox): + return default_points, False + + blocked_bbox = _bbox_payload(endpoint_bbox, clearance=0.0) + if not _route_points_hit_bbox(default_points, blocked_bbox): + return default_points, False + + avoid_clearance = DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE + 1.0 + expanded_bbox = _bbox_payload(endpoint_bbox, clearance=DEFAULT_NODE_TOLERANCE) + candidates = [] + candidates.extend(_orthogonal_access_point_candidates(start, end)) + candidates.extend(_terminal_access_dogleg_candidates(start, end, endpoint_bbox, avoid_clearance)) + valid = [ + points + for points in candidates + if len(points) >= 2 and not _route_points_hit_bbox(points, expanded_bbox) + ] + if not valid: + return default_points, False + valid.sort(key=_route_length) + return valid[0], True + + def _is_primary_route_carrier(carrier): kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND return kind in { @@ -3699,6 +4199,17 @@ def _is_primary_route_carrier(carrier): } +def _is_terminal_access_main_path_target(carrier): + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + return kind in { + ROUTE_CARRIER_KIND, + ROUTE_CARRIER_KIND_WIRE_DUCT, + ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ROUTE_CARRIER_KIND_USER_PATH, + } + + def _component_metrics_by_node(network): nodes = network.get("nodes", {}) if isinstance(network, dict) else {} edges = network.get("edges", {}) if isinstance(network, dict) else {} @@ -3869,13 +4380,103 @@ def _terminal_access_target_candidate(network, exit_point, max_distance): ranked = rank_connection_point_candidates(network, candidates) if not ranked: return None - return ranked[0] + nearest_physical = min( + candidates, + key=lambda candidate: float(candidate.get("distance", 0.0) or 0.0), + ) + main_path_candidates = [ + candidate + for candidate in ranked + if _is_terminal_access_main_path_target(candidate.get("carrier")) + ] + if main_path_candidates: + main_path_candidates.sort( + key=lambda candidate: ( + -int(candidate.get("route_entry_component_primary_segments", 0) or 0), + float(candidate.get("distance", 0.0) or 0.0), + ) + ) + selected = dict(main_path_candidates[0]) + selected["terminal_access_target_rule"] = ( + "main_path_preferred_over_fallback" + if not _is_terminal_access_main_path_target(nearest_physical.get("carrier")) + else "main_path_nearest" + ) + selected["terminal_access_fallback_target"] = False + return selected + selected = dict(ranked[0]) + selected["terminal_access_target_rule"] = "fallback_only" + selected["terminal_access_fallback_target"] = True + return selected + + +def _set_terminal_access_target_metadata(carrier, candidate): + if carrier is None or not isinstance(candidate, dict): + return + target_carrier = candidate.get("carrier") + target_kind = (getattr(target_carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + target_name = (getattr(target_carrier, "Name", "") or "").strip() + target_label = (getattr(target_carrier, "Label", "") or "").strip() or target_name + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessTargetKind", + PROPERTY_GROUP, + "Carrier kind selected as terminal access target", + target_kind, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessTargetName", + PROPERTY_GROUP, + "Carrier name selected as terminal access target", + target_name, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessTargetLabel", + PROPERTY_GROUP, + "Carrier label selected as terminal access target", + target_label, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessTargetRule", + PROPERTY_GROUP, + "Why this carrier was selected as terminal access target", + str(candidate.get("terminal_access_target_rule", "") or ""), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessFallbackTarget", + PROPERTY_GROUP, + "Whether the terminal access target is only a fallback carrier", + "1" if bool(candidate.get("terminal_access_fallback_target", False)) else "0", + ) + _ensure_float_property( + carrier, + "QetTerminalAccessTargetDistanceMm", + "Distance from terminal local exit to selected access target", + float(candidate.get("distance", 0.0) or 0.0), + ) + _ensure_integer_property( + carrier, + "QetTerminalAccessTargetComponentPrimarySegments", + "Primary route segment count in the selected target component", + int(candidate.get("route_entry_component_primary_segments", 0) or 0), + ) + _ensure_integer_property( + carrier, + "QetTerminalAccessTargetComponentSegments", + "Route segment count in the selected target component", + int(candidate.get("route_entry_component_segments", 0) or 0), + ) def create_terminal_access_carriers_from_document( doc, project_uuid="", terminal_exit_length=20.0, + terminal_exit_max_length=DEFAULT_TERMINAL_EXIT_MAX_LENGTH, max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, ): """Connect every engineering terminal to the generated route network. @@ -3910,7 +4511,11 @@ def create_terminal_access_carriers_from_document( if _live_source_carrier(doc, terminal) is not None: continue has_local_route_points = bool(_terminal_local_route_points(terminal)) - terminal_access_points = terminal_access_path_points(terminal, terminal_exit_length) + terminal_access_points = terminal_access_path_points( + terminal, + terminal_exit_length, + max_exit_length=terminal_exit_max_length, + ) if len(terminal_access_points) < 2: continue exit_point = terminal_access_points[-1] @@ -3924,13 +4529,19 @@ def create_terminal_access_carriers_from_document( if float(distance or 0.0) <= DEFAULT_NODE_TOLERANCE: continue + endpoint_bbox = _terminal_parent_device_bbox(terminal, _vector(TerminalObjects.terminal_origin(terminal))) + access_to_target_points, avoided_endpoint_device = _terminal_access_points_to_target( + exit_point, + nearest_point, + endpoint_bbox=endpoint_bbox, + ) if has_local_route_points: points = list(terminal_access_points) - for point in _orthogonal_access_points(exit_point, nearest_point)[1:]: + for point in access_to_target_points[1:]: if _distance(points[-1], point) > DEFAULT_NODE_TOLERANCE: points.append(point) else: - points = _orthogonal_access_points(exit_point, nearest_point) + points = access_to_target_points if len(points) < 2: continue label = getattr(terminal, "Label", "") or getattr(terminal, "Name", "") or "Terminal" @@ -3942,6 +4553,14 @@ def create_terminal_access_carriers_from_document( kind=ROUTE_CARRIER_KIND_TERMINAL_ACCESS, ) _mark_terminal_access_source(terminal, carrier) + _set_terminal_access_target_metadata(carrier, candidate) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessAvoidedEndpointDevice", + PROPERTY_GROUP, + "Whether TerminalAccess detoured around the terminal parent device bbox", + "1" if avoided_endpoint_device else "0", + ) created.append(carrier) return created @@ -3951,6 +4570,7 @@ def create_routing_path_network_from_document( project_uuid="", selection_ex=None, terminal_exit_length=20.0, + terminal_exit_max_length=DEFAULT_TERMINAL_EXIT_MAX_LENGTH, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): @@ -3997,6 +4617,7 @@ def create_routing_path_network_from_document( doc, project_uuid=project_uuid, terminal_exit_length=terminal_exit_length, + terminal_exit_max_length=terminal_exit_max_length, max_distance=terminal_access_max_distance, ) all_wire_duct_created = list(selected_wire_ducts) + list(wire_ducts) @@ -5292,6 +5913,66 @@ def _invalid_route_carriers(network): return invalid +def _terminal_for_access_carrier(carrier): + doc = getattr(carrier, "Document", None) + carrier_name = (getattr(carrier, "Name", "") or "").strip() + if doc is None or not carrier_name: + return None + for terminal in _collect_routable_terminals(doc): + if (getattr(terminal, "QetRouteCarrierName", "") or "").strip() == carrier_name: + return terminal + return None + + +def _terminal_access_diagnostic_payload(carrier): + terminal = _terminal_for_access_carrier(carrier) + access_points = _normalized_route_points(_carrier_points(carrier)) + payload = { + "access_carrier_name": getattr(carrier, "Name", "") or "", + "access_carrier_label": getattr(carrier, "Label", "") or "", + "target_kind": (getattr(carrier, "QetTerminalAccessTargetKind", "") or "").strip(), + "target_name": (getattr(carrier, "QetTerminalAccessTargetName", "") or "").strip(), + "target_label": (getattr(carrier, "QetTerminalAccessTargetLabel", "") or "").strip(), + "target_rule": (getattr(carrier, "QetTerminalAccessTargetRule", "") or "").strip(), + "target_distance_mm": float(getattr(carrier, "QetTerminalAccessTargetDistanceMm", 0.0) or 0.0), + "access_length_mm": float(_route_length(access_points)), + "access_points": [_point_payload(point) for point in access_points], + } + if terminal is not None: + terminal_payload = _terminal_diagnostic_payload(terminal) + payload.update( + { + "terminal_name": terminal_payload.get("name", ""), + "terminal_label": terminal_payload.get("label", ""), + "terminal_uuid": terminal_payload.get("terminal_uuid", ""), + "instance_id": terminal_payload.get("instance_id", ""), + "parent_device_name": terminal_payload.get("parent_device_name", ""), + "parent_device_label": terminal_payload.get("parent_device_label", ""), + "parent_device_instance_id": terminal_payload.get("parent_device_instance_id", ""), + "parent_device_element_uuid": terminal_payload.get("parent_device_element_uuid", ""), + } + ) + return payload + + +def _terminal_access_quality_diagnostics(network): + fallback_targets = [] + endpoint_device_avoidance = [] + for carrier in network.get("carriers", []) or []: + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() + if kind != ROUTE_CARRIER_KIND_TERMINAL_ACCESS: + continue + if str(getattr(carrier, "QetTerminalAccessFallbackTarget", "") or "").strip() == "1": + payload = _terminal_access_diagnostic_payload(carrier) + payload["code"] = "terminal_access_fallback_target" + fallback_targets.append(payload) + if str(getattr(carrier, "QetTerminalAccessAvoidedEndpointDevice", "") or "").strip() == "1": + payload = _terminal_access_diagnostic_payload(carrier) + payload["code"] = "terminal_access_endpoint_device_avoidance" + endpoint_device_avoidance.append(payload) + return fallback_targets, endpoint_device_avoidance + + def _cabinet_interior_boundary_bboxes(doc): bboxes = [] for obj in list(getattr(doc, "Objects", []) or []): @@ -5447,6 +6128,7 @@ def _terminal_access_geometry_payload(access_points): def diagnose_routing_path_network( doc, terminal_exit_length=20.0, + terminal_exit_max_length=DEFAULT_TERMINAL_EXIT_MAX_LENGTH, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, terminal_access_warning_distance=0.0, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, @@ -5461,6 +6143,9 @@ def diagnose_routing_path_network( isolated_components = _actionable_isolated_components(components) unconnected_terminals = [] long_terminal_accesses = [] + capped_terminal_exits = [] + corrected_terminal_exits = [] + invalid_terminal_exit_directions = [] invalid_terminal_local_routes = [] routing_range_only_network = _routing_range_only_network_payload(summary) boundary_bboxes = _cabinet_interior_boundary_bboxes(doc) @@ -5473,10 +6158,32 @@ def diagnose_routing_path_network( else: warning_distance = min(max(max_distance * 0.5, DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE), max_distance) if max_distance > 0.0 else DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE for terminal in routable_terminals: + exit_direction_issue = _terminal_exit_direction_issue(terminal) + if exit_direction_issue is not None: + invalid_terminal_exit_directions.append(exit_direction_issue) local_route_issue = _terminal_local_route_issue(terminal) if local_route_issue is not None: invalid_terminal_local_routes.append(local_route_issue) - terminal_access_points = terminal_access_path_points(terminal, terminal_exit_length) + access_diagnostics = terminal_access_diagnostics( + terminal, + exit_length=terminal_exit_length, + max_exit_length=terminal_exit_max_length, + ) + if access_diagnostics.get("exit_direction_corrected"): + corrected_payload = _terminal_diagnostic_payload(terminal) + corrected_payload.update(access_diagnostics) + corrected_payload["code"] = "terminal_exit_direction_corrected" + corrected_terminal_exits.append(corrected_payload) + if access_diagnostics.get("exit_length_capped"): + capped_payload = _terminal_diagnostic_payload(terminal) + capped_payload.update(access_diagnostics) + capped_payload["code"] = "terminal_exit_length_capped" + capped_terminal_exits.append(capped_payload) + terminal_access_points = terminal_access_path_points( + terminal, + terminal_exit_length, + max_exit_length=terminal_exit_max_length, + ) exit_point = terminal_access_points[-1] if terminal_access_points else _terminal_exit_point(terminal, terminal_exit_length) nearest_point, distance = nearest_point_on_network(network, exit_point) access_carrier = _live_source_carrier(doc, terminal) @@ -5517,6 +6224,9 @@ def diagnose_routing_path_network( possible_breaks = _wire_duct_endpoint_breaks(network) wire_ducts_without_terminal_access = _wire_duct_components_without_terminal_access(components, network) + terminal_access_fallback_targets, terminal_access_endpoint_device_avoidance = ( + _terminal_access_quality_diagnostics(network) + ) invalid_route_carriers = _invalid_route_carriers(network) route_carriers_outside_boundary = _route_carriers_outside_boundary(network, boundary_bboxes) terminals_outside_boundary = _terminals_outside_boundary( @@ -5570,6 +6280,24 @@ def diagnose_routing_path_network( "count": len(wire_ducts_without_terminal_access), } ) + if terminal_access_fallback_targets: + issues.append( + { + "severity": "warning", + "code": "terminal_access_fallback_targets", + "message": "Some terminal access carriers connect to fallback routing ranges instead of main paths.", + "count": len(terminal_access_fallback_targets), + } + ) + if terminal_access_endpoint_device_avoidance: + issues.append( + { + "severity": "info", + "code": "terminal_access_endpoint_device_avoidance", + "message": "Some terminal access carriers detoured around endpoint device bounding boxes.", + "count": len(terminal_access_endpoint_device_avoidance), + } + ) if long_terminal_accesses: issues.append( { @@ -5579,6 +6307,33 @@ def diagnose_routing_path_network( "count": len(long_terminal_accesses), } ) + if capped_terminal_exits: + issues.append( + { + "severity": "warning", + "code": "terminal_exit_length_capped", + "message": "Some terminal exit segments were capped before leaving the device bounding box.", + "count": len(capped_terminal_exits), + } + ) + if corrected_terminal_exits: + issues.append( + { + "severity": "info", + "code": "terminal_exit_direction_corrected", + "message": "Some default terminal exit directions were corrected before routing.", + "count": len(corrected_terminal_exits), + } + ) + if invalid_terminal_exit_directions: + issues.append( + { + "severity": "warning", + "code": "invalid_terminal_exit_directions", + "message": "Some terminals have invalid explicit exit direction metadata.", + "count": len(invalid_terminal_exit_directions), + } + ) if invalid_terminal_local_routes: issues.append( { @@ -5632,6 +6387,9 @@ def diagnose_routing_path_network( "isolated_components": isolated_components, "unconnected_terminals": unconnected_terminals, "long_terminal_accesses": long_terminal_accesses, + "capped_terminal_exits": capped_terminal_exits, + "corrected_terminal_exits": corrected_terminal_exits, + "invalid_terminal_exit_directions": invalid_terminal_exit_directions, "invalid_terminal_local_routes": invalid_terminal_local_routes, "routing_range_only_network": routing_range_only_network, "invalid_route_carriers": invalid_route_carriers, @@ -5639,6 +6397,8 @@ def diagnose_routing_path_network( "terminals_outside_boundary": terminals_outside_boundary, "possible_breaks": possible_breaks, "wire_ducts_without_terminal_access": wire_ducts_without_terminal_access, + "terminal_access_fallback_targets": terminal_access_fallback_targets, + "terminal_access_endpoint_device_avoidance": terminal_access_endpoint_device_avoidance, "issues": issues, "issue_codes": _diagnostic_issue_codes(issues), "ok": not issues, @@ -5661,6 +6421,24 @@ def _highlight_routing_network_diagnostics(doc, diagnostic): if item.get("name", "") ) unconnected_terminal_names.update(long_access_terminal_names) + capped_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("capped_terminal_exits", []) or [] + if item.get("name", "") + ) + unconnected_terminal_names.update(capped_terminal_names) + corrected_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("corrected_terminal_exits", []) or [] + if item.get("name", "") + ) + unconnected_terminal_names.update(corrected_terminal_names) + invalid_exit_direction_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("invalid_terminal_exit_directions", []) or [] + if item.get("name", "") + ) + unconnected_terminal_names.update(invalid_exit_direction_terminal_names) invalid_local_route_terminal_names = set( item.get("name", "") for item in diagnostic.get("invalid_terminal_local_routes", []) or [] @@ -5786,6 +6564,27 @@ def _routing_path_network_diagnostic_message(diagnostic): _diagnostic_terminal_text(sample), _diagnostic_distance_text(sample.get("terminal_access_length_mm")), ) + capped_exits = _diagnostic_items(diagnostic.get("capped_terminal_exits", []) or []) + if capped_exits: + sample = capped_exits[0] + message += "\n端子出线长度截断:{0},实际 {1} / 上限 {2}。".format( + _diagnostic_terminal_text(sample), + _diagnostic_distance_text(sample.get("actual_exit_length_mm")), + _diagnostic_distance_text(sample.get("max_exit_length_mm")), + ) + corrected_exits = _diagnostic_items(diagnostic.get("corrected_terminal_exits", []) or []) + if corrected_exits: + sample = corrected_exits[0] + message += "\n端子默认出线方向已校正:{0},建议复查设备端子 LCS 或模板出线方向。".format( + _diagnostic_terminal_text(sample) + ) + invalid_exit_directions = _diagnostic_items(diagnostic.get("invalid_terminal_exit_directions", []) or []) + if invalid_exit_directions: + sample = invalid_exit_directions[0] + message += "\n端子出线方向无效:{0},属性 {1}。".format( + _diagnostic_terminal_text(sample), + sample.get("property_name", "QetTerminalExitDirectionJson"), + ) invalid_carriers = _diagnostic_items(diagnostic.get("invalid_route_carriers", []) or []) if invalid_carriers: sample = invalid_carriers[0] @@ -5856,10 +6655,13 @@ _ROUTING_PATH_NETWORK_ISSUE_LABELS = { "empty_routing_path_network": "布线路径网络为空", "invalid_route_carriers": "路径对象几何无效", "routing_range_only_network": "仅使用布线面兜底", + "invalid_terminal_exit_directions": "端子出线方向无效", "invalid_terminal_local_routes": "端子局部路径无效", "route_carriers_outside_boundary": "路径越出柜内边界", "terminals_outside_boundary": "端子越出柜内边界", "long_terminal_accesses": "端子接入过长", + "terminal_exit_length_capped": "端子出线长度截断", + "terminal_exit_direction_corrected": "端子默认出线方向校正", "unconnected_terminals": "端子未接入", "wire_duct_endpoint_breaks": "线槽端点疑似断开", "wire_ducts_without_terminal_access": "线槽未接入端子主网络", @@ -5884,6 +6686,7 @@ def write_routing_path_network_diagnostic( doc, project_uuid="", terminal_exit_length=20.0, + terminal_exit_max_length=DEFAULT_TERMINAL_EXIT_MAX_LENGTH, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, terminal_access_warning_distance=0.0, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, @@ -5891,6 +6694,7 @@ def write_routing_path_network_diagnostic( diagnostic = diagnose_routing_path_network( doc, terminal_exit_length=terminal_exit_length, + terminal_exit_max_length=terminal_exit_max_length, terminal_access_max_distance=terminal_access_max_distance, terminal_access_warning_distance=terminal_access_warning_distance, adjoining_duct_tolerance=adjoining_duct_tolerance, diff --git a/src/Mod/FreeCADExchange/StaleObjectSync.py b/src/Mod/FreeCADExchange/StaleObjectSync.py index 5960664..7db16af 100644 --- a/src/Mod/FreeCADExchange/StaleObjectSync.py +++ b/src/Mod/FreeCADExchange/StaleObjectSync.py @@ -64,9 +64,7 @@ def _payload_identity_sets(payload): break for item in payload.get("devices", []) or []: - instance_id = _string_value(item, "device_instance_id") or _string_value( - item, "instance_id" - ) + instance_id = _string_value(item, "device_instance_id") if instance_id: device_instance_ids.add(instance_id) for terminal in item.get("terminals", []) or []: @@ -74,11 +72,6 @@ def _payload_identity_sets(payload): if terminal_uuid: terminal_uuids.add(terminal_uuid) - for item in payload.get("terminals", []) or []: - terminal_uuid = _string_value(item, "terminal_uuid") - if terminal_uuid: - terminal_uuids.add(terminal_uuid) - for item in payload.get("wires", []) or []: wire_uuid = ( _string_value(item, "wire_id") diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index d33b888..6329853 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -38,10 +38,7 @@ def _normalize_terminal_entry(item, index): "Terminal entry #{0} is missing terminal_uuid.".format(index) ) - instance_id = ( - (item.get("device_instance_id") or "").strip() - or (item.get("instance_id") or "").strip() - ) + device_instance_id = (item.get("device_instance_id") or "").strip() element_uuid = (item.get("element_uuid") or "").strip() terminal_display = (item.get("terminal_display") or "").strip() slot_name_hint = ( @@ -55,7 +52,7 @@ def _normalize_terminal_entry(item, index): return { "terminal_uuid": terminal_uuid, - "instance_id": instance_id, + "device_instance_id": device_instance_id, "element_uuid": element_uuid, "terminal_display": terminal_display, "slot_name_hint": slot_name_hint, @@ -70,10 +67,7 @@ def _payload_device_lookup(payload): if not isinstance(item, dict): continue - instance_id = ( - (item.get("device_instance_id") or "").strip() - or (item.get("instance_id") or "").strip() - ) + instance_id = (item.get("device_instance_id") or "").strip() if instance_id: by_instance_id.add(instance_id) @@ -99,10 +93,7 @@ def _payload_device_instance_by_element(payload): for device in payload.get("devices", []) or []: if not isinstance(device, dict): continue - device_instance_id = ( - (device.get("device_instance_id") or "").strip() - or (device.get("instance_id") or "").strip() - ) + device_instance_id = (device.get("device_instance_id") or "").strip() if not device_instance_id: continue for terminal in device.get("terminals", []) or []: @@ -111,43 +102,26 @@ def _payload_device_instance_by_element(payload): element_uuid = (terminal.get("element_uuid") or "").strip() if element_uuid and element_uuid not in result: result[element_uuid] = device_instance_id - for terminal in payload.get("terminals", []) or []: - if not isinstance(terminal, dict): - continue - element_uuid = (terminal.get("element_uuid") or "").strip() - device_instance_id = ( - (terminal.get("device_instance_id") or "").strip() - or (terminal.get("instance_id") or "").strip() - ) - if element_uuid and device_instance_id and element_uuid not in result: - result[element_uuid] = device_instance_id return result def _payload_terminal_entries(payload): if "terminals" in payload and payload.get("terminals") is not None: - terminal_entries = payload.get("terminals", []) - if not isinstance(terminal_entries, list): - raise TerminalImportError("Field 'terminals' must be a list.") - return list(terminal_entries) + raise TerminalImportError( + "Field 'terminals' at the JSON root is no longer supported. Use devices[].terminals[]." + ) terminal_entries = [] for device in payload.get("devices", []) or []: if not isinstance(device, dict): continue - device_instance_id = ( - (device.get("device_instance_id") or "").strip() - or (device.get("instance_id") or "").strip() - ) + device_instance_id = (device.get("device_instance_id") or "").strip() for terminal in device.get("terminals", []) or []: if not isinstance(terminal, dict): continue entry = dict(terminal) - if device_instance_id and not ( - (entry.get("device_instance_id") or "").strip() - or (entry.get("instance_id") or "").strip() - ): - entry["instance_id"] = device_instance_id + if device_instance_id: + entry["device_instance_id"] = device_instance_id terminal_entries.append(entry) return terminal_entries @@ -163,8 +137,7 @@ def _device_embedded_terminal_entries(payload, existing_keys): if not isinstance(device, dict): continue - device_element_uuid = (device.get("element_uuid") or "").strip() - device_instance_id = (device.get("instance_id") or "").strip() + device_instance_id = (device.get("device_instance_id") or "").strip() device_terminals = device.get("terminals", []) or [] if not isinstance(device_terminals, list): continue @@ -173,9 +146,8 @@ def _device_embedded_terminal_entries(payload, existing_keys): if not isinstance(terminal, dict): continue terminal_uuid = (terminal.get("terminal_uuid") or "").strip() - element_uuid = (terminal.get("element_uuid") or "").strip() or device_element_uuid - instance_id = (terminal.get("instance_id") or "").strip() or device_instance_id - if not terminal_uuid or not (element_uuid or instance_id): + element_uuid = (terminal.get("element_uuid") or "").strip() + if not terminal_uuid or not (element_uuid or device_instance_id): continue # QET 的正式端子可能直接挂在 devices[].terminals[] 下。 @@ -194,7 +166,7 @@ def _device_embedded_terminal_entries(payload, existing_keys): { "terminal_uuid": terminal_uuid, "element_uuid": element_uuid, - "instance_id": instance_id, + "device_instance_id": device_instance_id, "terminal_display": terminal_display, "slot_name_hint": terminal_display, } @@ -216,10 +188,8 @@ def _wire_endpoint_terminal_entries(payload, existing_keys): for side in ("start", "end"): terminal_uuid = (wire.get("{0}_terminal_uuid".format(side)) or "").strip() element_uuid = (wire.get("{0}_element_uuid".format(side)) or "").strip() - instance_id = (wire.get("{0}_instance_id".format(side)) or "").strip() - if not instance_id and element_uuid: - instance_id = instance_by_element.get(element_uuid, "") - if not terminal_uuid or not (element_uuid or instance_id): + device_instance_id = instance_by_element.get(element_uuid, "") + if not terminal_uuid or not (element_uuid or device_instance_id): continue key = (element_uuid, terminal_uuid) @@ -235,7 +205,7 @@ def _wire_endpoint_terminal_entries(payload, existing_keys): { "terminal_uuid": terminal_uuid, "element_uuid": element_uuid, - "instance_id": instance_id, + "device_instance_id": device_instance_id, "terminal_display": terminal_display, "slot_name_hint": terminal_display, } @@ -244,7 +214,7 @@ def _wire_endpoint_terminal_entries(payload, existing_keys): def _terminal_belongs_to_payload_devices(entry, device_lookup): - instance_id = entry["instance_id"] + instance_id = entry["device_instance_id"] element_uuid = entry["element_uuid"] if instance_id and instance_id in device_lookup["instance_ids"]: @@ -324,7 +294,7 @@ def _device_key(device_group): def _locate_device_group(doc, entry): - instance_id = entry["instance_id"] + instance_id = entry["device_instance_id"] element_uuid = entry["element_uuid"] device_group = None diff --git a/src/Mod/FreeCADExchange/TerminalObjects.py b/src/Mod/FreeCADExchange/TerminalObjects.py index 5f59081..e4cc32f 100644 --- a/src/Mod/FreeCADExchange/TerminalObjects.py +++ b/src/Mod/FreeCADExchange/TerminalObjects.py @@ -419,7 +419,50 @@ def _normalized_direction(vector): return App.Vector(vector.x / length, vector.y / length, vector.z / length) +def _direction_from_payload(value): + if isinstance(value, dict): + try: + return App.Vector( + float(value.get("x", 0.0) or 0.0), + float(value.get("y", 0.0) or 0.0), + float(value.get("z", 0.0) or 0.0), + ) + except Exception: + return None + if isinstance(value, (list, tuple)) and len(value) >= 3: + try: + return App.Vector(float(value[0] or 0.0), float(value[1] or 0.0), float(value[2] or 0.0)) + except Exception: + return None + return None + + +def _explicit_terminal_exit_direction(obj): + for property_name in ("QetTerminalExitDirectionJson", "QetExitDirectionJson"): + raw = str(getattr(obj, property_name, "") or "").strip() + if not raw: + continue + parsed = None + try: + parsed = json.loads(raw) + except Exception: + parts = [part.strip() for part in raw.replace(";", ",").split(",")] + if len(parts) >= 3: + parsed = parts[:3] + direction = _direction_from_payload(parsed) + if direction is None: + continue + normalized = _normalized_direction(direction) + if abs(normalized.x) + abs(normalized.y) + abs(normalized.z) > 1e-9: + return normalized + return None + + def terminal_direction(obj): + explicit_direction = _explicit_terminal_exit_direction(obj) + if explicit_direction is not None: + return explicit_direction + direction = App.Vector(0, 0, 1) try: @@ -464,6 +507,13 @@ def terminal_direction(obj): return App.Vector(0, 0, 1) +def terminal_direction_source(obj): + """Return where the terminal exit direction currently comes from.""" + if _explicit_terminal_exit_direction(obj) is not None: + return "explicit" + return "lcs" + + def create_lcs_object(doc, name_hint, placement=None, label=None): base_name = safe_token(name_hint) or "QETTerminal" object_name = _unique_object_name(doc, base_name) diff --git a/src/Mod/FreeCADExchange/WiringImport.py b/src/Mod/FreeCADExchange/WiringImport.py index e0272c1..86fa4ce 100644 --- a/src/Mod/FreeCADExchange/WiringImport.py +++ b/src/Mod/FreeCADExchange/WiringImport.py @@ -47,13 +47,46 @@ def _device_display_map(payload): for item in payload.get("devices", []) or []: if not isinstance(item, dict): continue - element_uuid = _string_value(item, "element_uuid") display_tag = _string_value(item, "display_tag") - if element_uuid and display_tag: + if not display_tag: + continue + + # 新交换协议中,一个 3D 设备实例可能合并多个 2D 符号; + # 导线端点仍用 element_uuid,所以这里要把组内所有 2D 符号都映射到同一设备标注。 + candidate_element_uuids = [] + for terminal in item.get("terminals", []) or []: + if not isinstance(terminal, dict): + continue + element_uuid = _string_value(terminal, "element_uuid") + if element_uuid: + candidate_element_uuids.append(element_uuid) + + for element_uuid in candidate_element_uuids: labels[element_uuid] = display_tag return labels +def _endpoint_instance_map(payload): + by_terminal = {} + by_element = {} + for device in payload.get("devices", []) or []: + if not isinstance(device, dict): + continue + device_instance_id = _string_value(device, "device_instance_id") + if not device_instance_id: + continue + for terminal in device.get("terminals", []) or []: + if not isinstance(terminal, dict): + continue + element_uuid = _string_value(terminal, "element_uuid") + terminal_uuid = _string_value(terminal, "terminal_uuid") + if element_uuid: + by_element.setdefault(element_uuid, device_instance_id) + if element_uuid and terminal_uuid: + by_terminal[(element_uuid, terminal_uuid)] = device_instance_id + return by_terminal, by_element + + def _endpoint_text(device_label, terminal_display, terminal_uuid): terminal = terminal_display or terminal_uuid if device_label and terminal: @@ -61,11 +94,12 @@ def _endpoint_text(device_label, terminal_display, terminal_uuid): return device_label or terminal or "未命名端子" -def _normalize_wire_entry(item, index, device_labels=None): +def _normalize_wire_entry(item, index, device_labels=None, endpoint_instances=None): if not isinstance(item, dict): raise WiringImportError("Wire entry #{0} must be an object.".format(index)) device_labels = device_labels or {} + endpoint_by_terminal, endpoint_by_element = endpoint_instances or ({}, {}) wire_uuid = ( _string_value(item, "wire_id") or _string_value(item, "wire_uuid") @@ -89,6 +123,14 @@ def _normalize_wire_entry(item, index, device_labels=None): end_element_uuid = _string_value(item, "end_element_uuid") start_terminal_display = _string_value(item, "start_terminal_display") end_terminal_display = _string_value(item, "end_terminal_display") + start_instance_id = endpoint_by_terminal.get( + (start_element_uuid, start_terminal_uuid), + endpoint_by_element.get(start_element_uuid, ""), + ) + end_instance_id = endpoint_by_terminal.get( + (end_element_uuid, end_terminal_uuid), + endpoint_by_element.get(end_element_uuid, ""), + ) start_device_label = _string_value(item, "start_device_label") or device_labels.get( start_element_uuid, "" ) @@ -109,12 +151,12 @@ def _normalize_wire_entry(item, index, device_labels=None): "wire_style_id": _int_text_value(item, "wire_style_id"), "start_element_uuid": start_element_uuid, "start_terminal_uuid": start_terminal_uuid, - "start_instance_id": _string_value(item, "start_instance_id"), + "start_instance_id": start_instance_id, "start_terminal_display": start_terminal_display, "start_device_label": start_device_label, "end_element_uuid": end_element_uuid, "end_terminal_uuid": end_terminal_uuid, - "end_instance_id": _string_value(item, "end_instance_id"), + "end_instance_id": end_instance_id, "end_terminal_display": end_terminal_display, "end_device_label": end_device_label, "endpoint_label": endpoint_label, @@ -243,9 +285,15 @@ def import_wire_tasks_from_payload(payload, doc=None): } device_labels = _device_display_map(payload) + endpoint_instances = _endpoint_instance_map(payload) for index, item in enumerate(wires): try: - entry = _normalize_wire_entry(item, index, device_labels=device_labels) + entry = _normalize_wire_entry( + item, + index, + device_labels=device_labels, + endpoint_instances=endpoint_instances, + ) except WiringImportError as exc: report["skipped_invalid"] += 1 report["warnings"].append(str(exc)) diff --git a/tests/manual/freecad_pending_device_scene_smoke.py b/tests/manual/freecad_pending_device_scene_smoke.py new file mode 100644 index 0000000..bf48e7f --- /dev/null +++ b/tests/manual/freecad_pending_device_scene_smoke.py @@ -0,0 +1,148 @@ +r"""Smoke test for QET pending-device scene persistence. + +Run with FreeCADCmd.exe, not system Python: + + D:\fc\run-FreeCAD-1.1.1\bin\FreeCADCmd.exe tests\manual\freecad_pending_device_scene_smoke.py +""" + +import json +import os +import shutil +import sys +import tempfile +from pathlib import Path + +import FreeCAD as App + + +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)) + +import DeviceImport # noqa: E402 + + +def _close_doc(doc): + if doc is None: + return + try: + App.closeDocument(doc.Name) + except Exception: + pass + + +def _make_source_model(path): + doc = App.newDocument("SmokeSourceModel") + try: + body = doc.addObject("Part::Box", "Body") + body.Length = 10 + body.Width = 8 + body.Height = 6 + doc.recompute() + doc.saveAs(str(path)) + finally: + _close_doc(doc) + + +def _assert_close(actual, expected, label): + if abs(float(actual) - float(expected)) > 1e-6: + raise AssertionError("{0}: expected {1}, got {2}".format(label, expected, actual)) + + +def main(): + temp_dir = Path(tempfile.mkdtemp(prefix="qet_pending_device_smoke_")) + try: + source_model = temp_dir / "device.FCStd" + scene_file = temp_dir / "QETScene.FCStd" + _make_source_model(source_model) + + scene = App.newDocument("QETSceneSmoke") + root = DeviceImport._ensure_root_group(scene, None, "project-smoke") + device_group, created = DeviceImport._ensure_device_group( + scene, + root, + "element-smoke", + "instance-smoke", + str(source_model), + "N600", + 0, + ) + if not created: + raise AssertionError("smoke device group should be newly created") + DeviceImport._set_device_assembly_state( + device_group, + DeviceImport.ASSEMBLY_STATE_PENDING, + ) + + mount_target = scene.addObject("App::Part", "MountingPlate") + mount_target.Label = "安装板" + mount_target.Placement = App.Placement(App.Vector(100, 200, 300), App.Rotation()) + DeviceImport._ensure_string_property( + mount_target, + "QetCarrierKind", + "QET Mount", + "Smoke mount target kind", + "mounting_plate", + ) + + DeviceImport.insert_pending_device( + scene, + device_group, + mount_target=mount_target, + mount_placement=App.Placement(App.Vector(10, 20, 30), App.Rotation()), + mount_normal=App.Vector(0, 0, 1), + mount_offset_mm=5.0, + ) + device_group_name = device_group.Name + scene.recompute() + scene.saveAs(str(scene_file)) + _close_doc(scene) + + reopened = App.openDocument(str(scene_file)) + try: + reopened_device = reopened.getObject(device_group_name) + if reopened_device is None: + raise AssertionError("reopened scene does not contain device group") + if getattr(reopened_device, "QetAssemblyState", "") != DeviceImport.ASSEMBLY_STATE_PLACED: + raise AssertionError("reopened device is not marked Placed") + _assert_close(reopened_device.Placement.Base.x, 10.0, "placement x") + _assert_close(reopened_device.Placement.Base.y, 20.0, "placement y") + _assert_close(reopened_device.Placement.Base.z, 35.0, "placement z") + if getattr(reopened_device, "QetMountHostName", "") != "MountingPlate": + raise AssertionError("mount host name was not persisted") + if getattr(reopened_device, "QetMountHostKind", "") != "mounting_plate": + raise AssertionError("mount host kind was not persisted") + if getattr(reopened_device, "QetMountOffsetMm", "") != "5.000000": + raise AssertionError("mount offset was not persisted") + normal_payload = json.loads(getattr(reopened_device, "QetMountHostNormalJson", "{}") or "{}") + _assert_close(normal_payload.get("z", 0.0), 1.0, "normal z") + finally: + _close_doc(reopened) + + result_path = os.environ.get("QET_PENDING_DEVICE_SMOKE_RESULT", "").strip() + if result_path: + Path(result_path).write_text( + json.dumps( + { + "ok": True, + "scene": str(scene_file), + "device": device_group_name, + "placement": {"x": 10.0, "y": 20.0, "z": 35.0}, + "assembly_state": DeviceImport.ASSEMBLY_STATE_PLACED, + "mount_host": "MountingPlate", + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + print("SMOKE_OK scene={0}".format(scene_file)) + return 0 + finally: + if os.environ.get("QET_KEEP_SMOKE_OUTPUT", "").strip() != "1": + shutil.rmtree(str(temp_dir), ignore_errors=True) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 6e321e8..395f615 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2599,6 +2599,218 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(0, second["main_path_detour_user_path_bridges"]) self.assertEqual(1, second["main_path_detour_bridge_duplicates"]) + def test_controller_creates_user_path_bridge_from_terminal_access_fallback_target(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") + fallback_source.Label = "安装板布线面" + fallback_duplicate = doc.addObject("Part::Feature", "CabinetRoutingRangeDuplicate") + fallback_duplicate.Label = "安装板布线面" + fallback_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面 carrier", + ) + duplicate_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(-500, 0, 0), app.Vector(-400, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面 duplicate carrier", + ) + main_path = routing_network.create_route_carrier( + doc, + [app.Vector(150, 20, 0), app.Vector(250, 20, 0)], + project_uuid="project-1", + kind="UserPath", + label="柜内主路径", + ) + fallback_carrier.QetRouteSourceName = fallback_source.Name + fallback_carrier.QetRouteSourceLabel = fallback_source.Label + duplicate_carrier.QetRouteSourceName = fallback_duplicate.Name + duplicate_carrier.QetRouteSourceLabel = fallback_duplicate.Label + main_path.QetRouteSourceLabel = "柜内主路径" + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "terminal_access_fallback_target_samples": [ + { + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_name": fallback_carrier.Name, + "target_label": "安装板布线面", + "target_distance": 35.0, + } + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + + first = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() + + bridges = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "TerminalAccessFallbackBridge" + ] + self.assertEqual(1, first["terminal_access_fallback_bridge_targets"]) + self.assertEqual(1, first["terminal_access_fallback_user_path_bridges"]) + self.assertEqual( + ["安装板布线面 -> 柜内主路径"], + first["terminal_access_fallback_bridge_pair_labels"], + ) + self.assertEqual(1, len(bridges)) + self.assertEqual("安装板布线面 -> 柜内主路径", bridges[0].QetRouteBridgePairLabel) + self.assertEqual(fallback_carrier.Name, bridges[0].QetRouteBridgeLeftSourceName) + self.assertEqual(main_path.Name, bridges[0].QetRouteBridgeRightSourceName) + self.assertEqual( + [(100.0, 0.0, 0.0), (150.0, 20.0, 0.0)], + [(point.x, point.y, point.z) for point in bridges[0].Points], + ) + + second = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() + + self.assertEqual(0, second["terminal_access_fallback_user_path_bridges"]) + self.assertEqual(1, second["terminal_access_fallback_bridge_duplicates"]) + + def test_controller_creates_terminal_access_fallback_bridge_from_path_network_diagnostic(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + fallback_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面", + ) + main_path = routing_network.create_route_carrier( + doc, + [app.Vector(150, 20, 0), app.Vector(250, 20, 0)], + project_uuid="project-1", + kind="UserPath", + label="柜内主路径", + ) + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "terminal_access_fallback_targets": [ + { + "target_kind": "RoutingRange", + "target_name": fallback_carrier.Name, + "target_label": "安装板布线面", + "target_distance": 35.0, + "terminal_uuid": "terminal-ud8-as", + "terminal_label": "as", + "parent_device_label": "UD:8", + } + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + + result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_terminal_access_fallback_targets() + + bridges = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "TerminalAccessFallbackBridge" + ] + self.assertEqual(1, result["terminal_access_fallback_bridge_targets"]) + self.assertEqual(1, result["terminal_access_fallback_user_path_bridges"]) + self.assertEqual(["安装板布线面 -> 柜内主路径"], result["terminal_access_fallback_bridge_pair_labels"]) + self.assertEqual(1, len(bridges)) + self.assertEqual(fallback_carrier.Name, bridges[0].QetRouteBridgeLeftSourceName) + self.assertEqual(main_path.Name, bridges[0].QetRouteBridgeRightSourceName) + + def test_controller_terminal_access_fallback_bridge_prefers_nearest_segment_not_endpoint(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + fallback_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面", + ) + near_segment_main_path = routing_network.create_route_carrier( + doc, + [app.Vector(50, 10, 0), app.Vector(50, 1000, 0)], + project_uuid="project-1", + kind="UserPath", + label="经过端子附近的长主路径", + ) + near_endpoint_but_farther_path = routing_network.create_route_carrier( + doc, + [app.Vector(120, 0, 0), app.Vector(130, 0, 0)], + project_uuid="project-1", + kind="UserPath", + label="端点较近但整体更远路径", + ) + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "terminal_access_fallback_target_samples": [ + { + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_name": fallback_carrier.Name, + "target_label": "安装板布线面", + "target_distance": 35.0, + } + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + + result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() + + bridges = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "TerminalAccessFallbackBridge" + ] + self.assertEqual(1, result["terminal_access_fallback_user_path_bridges"]) + self.assertEqual(1, len(bridges)) + self.assertEqual(near_segment_main_path.Name, bridges[0].QetRouteBridgeRightSourceName) + self.assertNotEqual(near_endpoint_but_farther_path.Name, bridges[0].QetRouteBridgeRightSourceName) + self.assertEqual( + [(50.0, 0.0, 0.0), (50.0, 10.0, 0.0)], + [(point.x, point.y, point.z) for point in bridges[0].Points], + ) + def test_controller_does_not_duplicate_diagnostic_user_path_bridge(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -2678,7 +2890,7 @@ class AutoRoutingTest(unittest.TestCase): ) self.assertIn("自动诊断桥接:生成 UserPath 1 条", message) - def test_route_eplan_connections_does_not_auto_create_diagnostic_bridge_by_default(self): + def test_route_eplan_connections_does_not_auto_create_diagnostic_bridge_when_disabled(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -2703,6 +2915,7 @@ class AutoRoutingTest(unittest.TestCase): report = auto_routing.route_eplan_connections( doc, payload={"project_uuid": "project-1", "wires": []}, + options={"auto_create_diagnostic_bridges": False}, project_uuid="project-1", update_network=False, ) @@ -3886,6 +4099,143 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(["wire-missing"], result["missing_issue_wire_refs"]) self.assertEqual([long_wire, boundary_wire], selected) + def test_controller_selects_wire_outside_boundary_wires(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled_boundary") + sampled_wire.Label = "N-OUT-A: A1 -> B1 (BoundaryWarning)" + sampled_wire.RouteType = "RoutedConnection" + sampled_wire.QetWireUuid = "wire-sampled-boundary" + object_issue_wire = doc.addObject("Part::Feature", "QETRoutedConnection_object_boundary") + object_issue_wire.Label = "N-OUT-B: A2 -> B2 (BoundaryWarning)" + object_issue_wire.RouteType = "RoutedConnection" + object_issue_wire.QetWireUuid = "wire-object-boundary" + object_issue_wire.QetRouteIssueCodes = "route_candidate_boundary_violations" + normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal_boundary") + normal_wire.Label = "N-OK: A3 -> B3 (Routed)" + normal_wire.RouteType = "RoutedConnection" + normal_wire.QetWireUuid = "wire-ok" + normal_wire.QetRouteIssueCodes = "" + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "wire_outside_boundary_samples": [ + { + "wire_uuid": "wire-sampled-boundary", + "wire_object_label": "N-OUT-A: A1 -> B1 (BoundaryWarning)", + }, + { + "wire_uuid": "wire-missing-boundary", + "wire_object_label": "N-MISSING: A4 -> B4 (BoundaryWarning)", + }, + ], + "route_samples": [ + { + "wire_uuid": "wire-ok", + "wire_object_label": "N-OK: A3 -> B3 (Routed)", + "issue_codes": [], + } + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], + ) + + result = auto_routing_panel.AutoRoutingController().select_wire_outside_boundary_wires() + + self.assertEqual(2, result["selected_wire_outside_boundary_wires"]) + self.assertEqual( + ["QETRoutedConnection_sampled_boundary", "QETRoutedConnection_object_boundary"], + result["selected_wire_outside_boundary_wire_names"], + ) + self.assertEqual(["wire-missing-boundary"], result["missing_wire_outside_boundary_refs"]) + self.assertEqual([sampled_wire, object_issue_wire], selected) + + def test_controller_selects_wire_outside_boundary_route_sources_from_wire_track(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + source_path = doc.addObject("Part::Feature", "OutsideUserPathSketch") + source_path.Label = "越界用户路径草图" + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="越界 UserPath", + project_uuid="project-1", + kind="UserPath", + ) + carrier.QetRouteSourceName = source_path.Name + carrier.QetRouteSourceLabel = source_path.Label + wire = doc.addObject("Part::Feature", "QETRoutedConnection_boundary_with_track") + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-boundary-track" + wire.QetRouteIssueCodes = "route_candidate_boundary_violations" + wire.QetRouteTrackJson = json.dumps( + { + "segments": [ + { + "carrier": { + "name": carrier.Name, + "label": carrier.Label, + "source_name": source_path.Name, + "source_label": source_path.Label, + } + } + ], + }, + ensure_ascii=False, + ) + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "wire_outside_boundary_samples": [ + {"wire_uuid": "wire-boundary-track"}, + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], + ) + + result = auto_routing_panel.AutoRoutingController().select_wire_outside_boundary_wires() + + self.assertEqual(1, result["selected_wire_outside_boundary_wires"]) + self.assertEqual(1, result["selected_wire_outside_boundary_route_carriers"]) + self.assertEqual(1, result["selected_wire_outside_boundary_route_sources"]) + self.assertEqual([carrier.Name], result["selected_wire_outside_boundary_route_carrier_names"]) + self.assertEqual([source_path.Name], result["selected_wire_outside_boundary_route_source_names"]) + self.assertEqual([wire, carrier, source_path], selected) + def test_controller_selects_main_path_detour_missing_wires(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() @@ -4325,59 +4675,51 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(["缺失补路位置"], result["missing_main_path_detour_rejected_fallback_refs"]) self.assertEqual([fallback_source], selected) - def test_controller_selects_main_path_detour_bridge_endpoint_sources_from_summary(self): + def test_controller_selects_terminal_access_fallback_targets_from_latest_batch_diagnostic(self): _install_fake_freecad() - terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - wiring_objects.ensure_diagnostic_group(doc, "project-1") - routed_group = wiring_objects.ensure_routed_group(doc, "project-1") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") - fallback_source.QetRouteSourceLabel = "安装板布线面" - current_source = doc.addObject("Part::Feature", "MainWireDuctSource") - current_source.QetRouteSourceLabel = "主线槽A" - wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_pair") - wire.Label = "N-PAIR: A1 -> B1" + fallback_source.Label = "安装板布线面" + duplicate_label_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSourceDuplicate") + duplicate_label_source.Label = "安装板布线面" + wire = doc.addObject("Part::Feature", "QETRoutedConnection_access_fallback") wire.RouteType = "RoutedConnection" - wire.QetWireUuid = "wire-main-path-pair" - wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" - wire.PropertiesList = [ - "QetStartTerminalUuid", - "QetEndTerminalUuid", - "QetRouteDiagnosticsJson", - "QetRouteTrackJson", - ] - wire.QetStartTerminalUuid = "terminal-a" - wire.QetEndTerminalUuid = "terminal-b" - wire.QetRouteDiagnosticsJson = json.dumps( - { - "selective_collision_reroute": { - "status": "RejectedFallback", - "rejected_fallback_kinds": ["RoutingRange"], - "rejected_fallback_labels": ["安装板布线面"], - } - }, - ensure_ascii=False, - ) - wire.QetRouteTrackJson = json.dumps( + wire.QetWireUuid = "wire-fallback" + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( { - "segments": [ + "terminal_access_fallback_target_samples": [ { - "carrier": { - "kind": "WireDuct", - "source_label": "主线槽A", - "source_name": current_source.Name, - } - } - ] + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_name": fallback_source.Name, + "target_label": "安装板布线面", + "target_distance": 35.0, + }, + { + "wire_uuid": "wire-missing", + "wire_label": "W404", + "endpoint": "end", + "target_kind": "RoutingRange", + "target_label": "缺失布线面", + "target_distance": 50.0, + }, + ], }, ensure_ascii=False, ) - routed_group.addObject(wire) + diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), @@ -4386,19 +4728,17 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_main_path_detour_rejected_fallback_sources() + result = auto_routing_panel.AutoRoutingController().select_terminal_access_fallback_targets() - self.assertEqual(2, result["selected_main_path_detour_bridge_endpoint_objects"]) - self.assertEqual(1, result["selected_main_path_detour_rejected_fallback_sources"]) - self.assertEqual(1, result["selected_main_path_detour_current_route_sources"]) - self.assertEqual([fallback_source.Name], result["selected_main_path_detour_rejected_fallback_source_names"]) - self.assertEqual([current_source.Name], result["selected_main_path_detour_current_route_source_names"]) - self.assertEqual({"主线槽A": 1}, result["main_path_detour_current_route_source_label_counts"]) - self.assertEqual({"安装板布线面 -> 主线槽A": 1}, result["main_path_detour_bridge_pair_counts"]) - self.assertEqual([], result["missing_main_path_detour_current_route_refs"]) - self.assertEqual([fallback_source, current_source], selected) + self.assertEqual(1, result["selected_terminal_access_fallback_wires"]) + self.assertEqual(1, result["selected_terminal_access_fallback_targets"]) + self.assertEqual([wire.Name], result["selected_terminal_access_fallback_wire_names"]) + self.assertEqual([fallback_source.Name], result["selected_terminal_access_fallback_target_names"]) + self.assertEqual(["wire-missing", "缺失布线面"], result["missing_terminal_access_fallback_refs"]) + self.assertEqual([wire, fallback_source], selected) + self.assertNotIn(duplicate_label_source, selected) - def test_controller_selects_issue_wires_from_wire_object_issue_codes(self): + def test_controller_selects_terminal_access_fallback_wires_by_issue_code_when_samples_are_limited(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4407,33 +4747,33 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled") - sampled_wire.Label = "N-SAMPLED: A1 -> B1 (BoundaryWarning)" + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled_access_fallback") sampled_wire.RouteType = "RoutedConnection" sampled_wire.QetWireUuid = "wire-sampled" - hidden_issue_wire = doc.addObject("Part::Feature", "QETRoutedConnection_hidden") - hidden_issue_wire.Label = "N-HIDDEN: A2 -> B2 (LongAccessWarning)" - hidden_issue_wire.RouteType = "RoutedConnection" - hidden_issue_wire.QetWireUuid = "wire-hidden" - hidden_issue_wire.QetRouteIssueCodes = "long_terminal_access" + hidden_wire = doc.addObject("Part::Feature", "QETRoutedConnection_hidden_access_fallback") + hidden_wire.RouteType = "RoutedConnection" + hidden_wire.QetWireUuid = "wire-hidden" + hidden_wire.QetRouteIssueCodes = "terminal_access_fallback_targets" normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") - normal_wire.Label = "N-OK: A3 -> B3 (Routed)" normal_wire.RouteType = "RoutedConnection" - normal_wire.QetWireUuid = "wire-ok" + normal_wire.QetWireUuid = "wire-normal" normal_wire.QetRouteIssueCodes = "" - diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "route_samples": [ + "terminal_access_fallback_target_samples": [ { "wire_uuid": "wire-sampled", - "wire_object_label": "N-SAMPLED: A1 -> B1 (BoundaryWarning)", - "issue_codes": ["boundary_warning"], - } - ] + "wire_label": "W1", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_label": "安装板布线面", + "target_distance": 35.0, + }, + ], }, ensure_ascii=False, ) @@ -4446,17 +4786,17 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_issue_wires() + result = auto_routing_panel.AutoRoutingController().select_terminal_access_fallback_targets() - self.assertEqual(2, result["selected_issue_wires"]) + self.assertEqual(2, result["selected_terminal_access_fallback_wires"]) self.assertEqual( - ["QETRoutedConnection_sampled", "QETRoutedConnection_hidden"], - result["selected_issue_wire_names"], + [sampled_wire.Name, hidden_wire.Name], + result["selected_terminal_access_fallback_wire_names"], ) - self.assertEqual([], result["missing_issue_wire_refs"]) - self.assertEqual([sampled_wire, hidden_issue_wire], selected) + self.assertEqual([sampled_wire, hidden_wire], selected) + self.assertNotIn(normal_wire, selected) - def test_controller_selects_long_terminal_accesses_from_latest_batch_diagnostic(self): + def test_controller_selects_terminal_access_carrier_for_fallback_endpoint(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4465,21 +4805,37 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - terminal_a = _terminal(doc, terminal_objects, "Terminal325", "terminal-325", app.Vector(0, 0, 0)) - terminal_b = _terminal(doc, terminal_objects, "Terminal326", "terminal-326", app.Vector(10, 0, 0)) diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") + fallback_source.Label = "安装板布线面" + access_carrier = doc.addObject("Part::Feature", "QETTerminalAccess_start") + access_carrier.QetRouteCarrierKind = "TerminalAccess" + access_carrier.Label = "起点端子接入" + wire = doc.addObject("Part::Feature", "QETRoutedConnection_access_fallback") + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-fallback" + wire.QetRouteNetworkJson = json.dumps( + { + "start_terminal_access_carrier": access_carrier.Name, + "end_terminal_access_carrier": "", + }, + ensure_ascii=False, + ) diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "routing_path_network_diagnostic": { - "long_terminal_accesses": [ - {"terminal_uuid": "terminal-325", "name": "Terminal325", "label": "325"}, - {"terminal_uuid": "terminal-326", "name": "Terminal326", "label": "326"}, - {"terminal_uuid": "terminal-404", "name": "Terminal404", "label": "404"}, - ] - } + "terminal_access_fallback_target_samples": [ + { + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_name": fallback_source.Name, + "target_label": "安装板布线面", + }, + ], }, ensure_ascii=False, ) @@ -4492,14 +4848,13 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_long_terminal_accesses() + result = auto_routing_panel.AutoRoutingController().select_terminal_access_fallback_targets() - self.assertEqual(2, result["selected_long_terminal_accesses"]) - self.assertEqual(["Terminal325", "Terminal326"], result["selected_long_terminal_names"]) - self.assertEqual(["terminal-404"], result["missing_long_terminal_refs"]) - self.assertEqual([terminal_a, terminal_b], selected) + self.assertEqual(1, result["selected_terminal_access_fallback_access_carriers"]) + self.assertEqual([access_carrier.Name], result["selected_terminal_access_fallback_access_carrier_names"]) + self.assertEqual([wire, fallback_source, access_carrier], selected) - def test_controller_selects_long_terminal_access_devices_from_latest_batch_diagnostic(self): + def test_controller_selects_terminal_access_fallback_terminal_and_device_from_samples(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4508,39 +4863,42 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - device_pen = doc.addObject("App::DocumentObjectGroup", "DevicePEN") - device_pen.Label = "PEN" - device_pe = doc.addObject("App::DocumentObjectGroup", "DevicePE") - device_pe.Label = "PE" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_UD8") + device.Label = "UD:8" + terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_UD8_as") + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "element-ud8", + "terminal-ud8-as", + "instance-ud8", + label="as", + ) + device.addObject(terminal) + fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") + fallback_source.Label = "安装板布线面" + wire = doc.addObject("Part::Feature", "QETRoutedConnection_access_fallback") + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-fallback" diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "routing_path_network_diagnostic": { - "long_terminal_accesses": [ - { - "terminal_uuid": "terminal-325", - "parent_device_name": "DevicePEN", - "parent_device_label": "PEN", - }, - { - "terminal_uuid": "terminal-326", - "parent_device_name": "DevicePEN", - "parent_device_label": "PEN", - }, - { - "terminal_uuid": "terminal-327", - "parent_device_label": "PE", - }, - { - "terminal_uuid": "terminal-404", - "parent_device_name": "Device404", - "parent_device_label": "404", - }, - ] - } + "terminal_access_fallback_target_samples": [ + { + "wire_uuid": "wire-fallback", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_name": fallback_source.Name, + "terminal_uuid": "terminal-ud8-as", + "terminal_name": terminal.Name, + "terminal_label": "as", + "parent_device_name": device.Name, + "parent_device_label": "UD:8", + }, + ], }, ensure_ascii=False, ) @@ -4553,14 +4911,15 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_long_terminal_access_devices() + result = auto_routing_panel.AutoRoutingController().select_terminal_access_fallback_targets() - self.assertEqual(2, result["selected_long_terminal_access_devices"]) - self.assertEqual(["DevicePEN", "DevicePE"], result["selected_long_terminal_access_device_names"]) - self.assertEqual(["Device404"], result["missing_long_terminal_access_device_refs"]) - self.assertEqual([device_pen, device_pe], selected) + self.assertEqual(1, result["selected_terminal_access_fallback_terminals"]) + self.assertEqual(1, result["selected_terminal_access_fallback_devices"]) + self.assertEqual([terminal.Name], result["selected_terminal_access_fallback_terminal_names"]) + self.assertEqual([device.Name], result["selected_terminal_access_fallback_device_names"]) + self.assertEqual([wire, fallback_source, terminal, device], selected) - def test_controller_selects_missing_terminal_devices_from_latest_batch_diagnostic(self): + def test_controller_selects_terminal_access_fallback_targets_from_path_network_diagnostic(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4569,37 +4928,50 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - device_a = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") - terminal_objects.ensure_string_property(device_a, "QetElementUuid", "QET Exchange", "", "device-a") - terminal_objects.ensure_string_property(device_a, "QetInstanceId", "QET Exchange", "", "instance-a") - device_b = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_b") - terminal_objects.ensure_string_property(device_b, "QetElementUuid", "QET Exchange", "", "device-b") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") - diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") - diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_UD8") + device.Label = "UD:8" + terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_UD8_as") + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "element-ud8", + "terminal-ud8-as", + "instance-ud8", + label="as", + ) + device.addObject(terminal) + fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") + fallback_source.Label = "安装板布线面" + access_carrier = doc.addObject("Part::Feature", "QETTerminalAccess_UD8_as") + access_carrier.QetRouteCarrierKind = "TerminalAccess" + access_carrier.Label = "UD:8/as 接入段" + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "missing_endpoint_samples": [ + "terminal_access_fallback_targets": [ { - "wire_uuid": "wire-a", - "start_found": False, - "start_terminal_uuid": "terminal-missing-a", - "start_instance_id": "instance-a", - "start_element_uuid": "device-a", - "end_found": True, + "target_kind": "RoutingRange", + "target_name": fallback_source.Name, + "target_label": "安装板布线面", + "access_carrier_name": access_carrier.Name, + "access_carrier_label": "UD:8/as 接入段", + "terminal_uuid": "terminal-ud8-as", + "terminal_name": terminal.Name, + "terminal_label": "as", + "parent_device_name": device.Name, + "parent_device_label": "UD:8", }, { - "wire_uuid": "wire-b", - "start_found": False, - "start_terminal_uuid": "terminal-missing-b", - "start_instance_id": "instance-missing", - "start_element_uuid": "device-missing", - "end_found": False, - "end_terminal_uuid": "terminal-missing-c", - "end_element_uuid": "device-b", + "target_kind": "RoutingRange", + "target_name": "MissingFallbackTarget", + "access_carrier_name": "MissingFallbackAccess", + "terminal_name": "MissingFallbackTerminal", + "parent_device_name": "MissingFallbackDevice", }, - ] + ], }, ensure_ascii=False, ) @@ -4612,14 +4984,24 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() + result = auto_routing_panel.AutoRoutingController().select_terminal_access_fallback_targets() - self.assertEqual(2, result["selected_missing_terminal_devices"]) - self.assertEqual(["QETDevice_device_a", "QETDevice_device_b"], result["selected_missing_terminal_device_names"]) - self.assertEqual(["terminal-missing-b"], result["missing_terminal_device_refs"]) - self.assertEqual([device_a, device_b], selected) + self.assertEqual(0, result["selected_terminal_access_fallback_wires"]) + self.assertEqual(1, result["selected_terminal_access_fallback_targets"]) + self.assertEqual(1, result["selected_terminal_access_fallback_access_carriers"]) + self.assertEqual(1, result["selected_terminal_access_fallback_terminals"]) + self.assertEqual(1, result["selected_terminal_access_fallback_devices"]) + self.assertEqual([fallback_source.Name], result["selected_terminal_access_fallback_target_names"]) + self.assertEqual([access_carrier.Name], result["selected_terminal_access_fallback_access_carrier_names"]) + self.assertEqual([terminal.Name], result["selected_terminal_access_fallback_terminal_names"]) + self.assertEqual([device.Name], result["selected_terminal_access_fallback_device_names"]) + self.assertEqual( + ["MissingFallbackTarget", "MissingFallbackAccess", "MissingFallbackTerminal", "MissingFallbackDevice"], + result["missing_terminal_access_fallback_refs"], + ) + self.assertEqual([fallback_source, access_carrier, terminal, device], selected) - def test_controller_selects_missing_terminal_device_by_device_label_fallback(self): + def test_controller_selects_terminal_access_endpoint_device_avoidance_from_path_diagnostic(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4628,23 +5010,48 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - device = doc.addObject("App::DocumentObjectGroup", "QETDevice_no_uuid") - device.Label = "缺端子设备A" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") - diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") - diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_UD8") + device.Label = "UD:8" + terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_UD8_as") + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "element-ud8", + "terminal-ud8-as", + "instance-ud8", + label="as", + ) + device.addObject(terminal) + target_path = doc.addObject("Part::Feature", "QETRouteCarrier_UserPathMain") + target_path.Label = "柜内主路径A" + access_carrier = doc.addObject("Part::Feature", "QETTerminalAccess_UD8_as") + access_carrier.QetRouteCarrierKind = "TerminalAccess" + access_carrier.Label = "UD:8/as 接入段" + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "missing_endpoint_samples": [ + "terminal_access_endpoint_device_avoidance": [ { - "wire_uuid": "wire-a", - "start_found": False, - "start_terminal_uuid": "terminal-missing-a", - "start_device_label": "缺端子设备A", - "end_found": True, - } - ] + "terminal_uuid": "terminal-ud8-as", + "terminal_name": terminal.Name, + "terminal_label": "as", + "parent_device_name": device.Name, + "parent_device_label": "UD:8", + "target_name": target_path.Name, + "target_label": "柜内主路径A", + "access_carrier_name": access_carrier.Name, + "access_carrier_label": "UD:8/as 接入段", + }, + { + "terminal_uuid": "terminal-missing", + "terminal_name": "MissingTerminal", + "target_name": "MissingPath", + "access_carrier_name": "MissingAccess", + }, + ], }, ensure_ascii=False, ) @@ -4657,14 +5064,24 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() + result = auto_routing_panel.AutoRoutingController().select_terminal_access_endpoint_device_avoidance() - self.assertEqual(1, result["selected_missing_terminal_devices"]) - self.assertEqual(["QETDevice_no_uuid"], result["selected_missing_terminal_device_names"]) - self.assertEqual([], result["missing_terminal_device_refs"]) - self.assertEqual([device], selected) + self.assertEqual(4, result["selected_terminal_access_endpoint_avoidance_objects"]) + self.assertEqual(1, result["selected_terminal_access_endpoint_avoidance_terminals"]) + self.assertEqual(1, result["selected_terminal_access_endpoint_avoidance_devices"]) + self.assertEqual(1, result["selected_terminal_access_endpoint_avoidance_targets"]) + self.assertEqual(1, result["selected_terminal_access_endpoint_avoidance_access_carriers"]) + self.assertEqual([terminal.Name], result["selected_terminal_access_endpoint_avoidance_terminal_names"]) + self.assertEqual([device.Name], result["selected_terminal_access_endpoint_avoidance_device_names"]) + self.assertEqual([target_path.Name], result["selected_terminal_access_endpoint_avoidance_target_names"]) + self.assertEqual( + [access_carrier.Name], + result["selected_terminal_access_endpoint_avoidance_access_carrier_names"], + ) + self.assertEqual(["MissingTerminal", "MissingPath", "MissingAccess"], result["missing_terminal_access_endpoint_avoidance_refs"]) + self.assertEqual([terminal, device, target_path, access_carrier], selected) - def test_controller_reports_missing_terminal_device_reason_counts_when_device_not_in_scene(self): + def test_controller_selects_unconnected_terminal_access_issues_from_path_diagnostic(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4674,25 +5091,37 @@ class AutoRoutingTest(unittest.TestCase): app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") - diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") - diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + device = doc.addObject("App::DocumentObjectGroup", "DeviceUnconnected") + device.Label = "未接入设备" + terminal = _terminal( + doc, + terminal_objects, + "TerminalUnconnected", + "terminal-unconnected", + app.Vector(0, 0, 0), + ) + device.addObject(terminal) + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "missing_endpoint_samples": [ + "unconnected_terminals": [ { - "wire_uuid": "wire-a", - "start_found": False, - "start_terminal_uuid": "terminal-missing-a", - "start_element_uuid": "device-a", - "start_instance_id": "instance-a", - "start_device_label": "UD:8", - "start_terminal_display": "as", - "start_missing_endpoint_reason_code": "device_not_in_3d_scene", - "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", - "end_found": True, - } - ] + "name": terminal.Name, + "label": "A1", + "terminal_uuid": "terminal-unconnected", + "parent_device_name": device.Name, + "parent_device_label": "未接入设备", + "nearest_network_distance_mm": 125.0, + "terminal_access_max_distance_mm": 50.0, + }, + { + "name": "MissingUnconnectedTerminal", + "terminal_uuid": "terminal-missing-unconnected", + "parent_device_name": "MissingUnconnectedDevice", + }, + ], }, ensure_ascii=False, ) @@ -4705,17 +5134,18 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() + result = auto_routing_panel.AutoRoutingController().select_unconnected_terminal_access_issues() - self.assertEqual(0, result["selected_missing_terminal_devices"]) - self.assertEqual(["terminal-missing-a"], result["missing_terminal_device_refs"]) - self.assertEqual(["UD:8"], result["missing_terminal_device_labels"]) - self.assertEqual(["instance-a"], result["missing_terminal_device_instance_ids"]) - self.assertEqual(["device-a"], result["missing_terminal_device_element_uuids"]) - self.assertEqual({"device_not_in_3d_scene": 1}, result["missing_terminal_device_reason_counts"]) - self.assertEqual([], selected) + self.assertEqual(2, result["selected_unconnected_terminal_access_objects"]) + self.assertEqual(1, result["selected_unconnected_terminal_access_terminals"]) + self.assertEqual(1, result["selected_unconnected_terminal_access_devices"]) + self.assertEqual([terminal.Name], result["selected_unconnected_terminal_access_terminal_names"]) + self.assertEqual([device.Name], result["selected_unconnected_terminal_access_device_names"]) + self.assertEqual(["MissingUnconnectedTerminal", "MissingUnconnectedDevice"], result["missing_unconnected_terminal_access_refs"]) + self.assertEqual(125.0, result["max_unconnected_terminal_access_distance_mm"]) + self.assertEqual([terminal, device], selected) - def test_controller_selects_found_counterpart_terminals_from_missing_endpoint_samples(self): + def test_controller_selects_terminal_exit_issue_terminals_from_path_diagnostic(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -4724,47 +5154,108 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - end_terminal = _terminal(doc, terminal_objects, "TerminalFoundEnd", "terminal-found-end", app.Vector(20, 0, 0)) - start_terminal = _terminal( + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + corrected_device = doc.addObject("App::DocumentObjectGroup", "DeviceCorrected") + corrected_device.Label = "方向校正设备" + corrected_terminal = _terminal( doc, terminal_objects, - "TerminalFoundStart", - "terminal-found-start", - app.Vector(40, 0, 0), + "TerminalCorrectedExit", + "terminal-corrected", + app.Vector(0, 0, 0), ) - diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") - diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") - diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" - diagnostic.QetProjectUuid = "project-1" - diagnostic.QetDiagnosticJson = json.dumps( - { - "missing_endpoint_samples": [ - { - "wire_uuid": "wire-a", - "wire_label": "F6", - "start_found": False, - "start_terminal_uuid": "terminal-missing-start", - "end_found": True, - "end_terminal_uuid": "terminal-found-end", - "end_terminal_display": "6", + corrected_device.addObject(corrected_terminal) + capped_device = doc.addObject("App::DocumentObjectGroup", "DeviceCapped") + capped_device.Label = "长度截断设备" + capped_terminal = _terminal( + doc, + terminal_objects, + "TerminalCappedExit", + "terminal-capped", + app.Vector(100, 0, 0), + ) + capped_device.addObject(capped_terminal) + invalid_device = doc.addObject("App::DocumentObjectGroup", "DeviceInvalidDirection") + invalid_device.Label = "方向无效设备" + invalid_terminal = _terminal( + doc, + terminal_objects, + "TerminalInvalidDirection", + "terminal-invalid-direction", + app.Vector(200, 0, 0), + ) + invalid_device.addObject(invalid_terminal) + invalid_local_device = doc.addObject("App::DocumentObjectGroup", "DeviceInvalidLocalRoute") + invalid_local_device.Label = "局部路径无效设备" + invalid_local_terminal = _terminal( + doc, + terminal_objects, + "TerminalInvalidLocalRoute", + "terminal-invalid-local-route", + app.Vector(300, 0, 0), + ) + invalid_local_device.addObject(invalid_local_terminal) + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "corrected_terminal_exits": [ + { + "name": corrected_terminal.Name, + "label": "A1", + "terminal_uuid": "terminal-corrected", + "parent_device_name": corrected_device.Name, + "parent_device_label": "方向校正设备", }, { - "wire_uuid": "wire-b", - "wire_label": "N2", - "start_found": True, - "start_terminal_uuid": "terminal-found-start", - "start_terminal_display": "1", - "end_found": False, - "end_terminal_uuid": "terminal-missing-end", + "name": "MissingCorrectedTerminal", + "terminal_uuid": "terminal-missing-corrected", + "parent_device_name": "MissingCorrectedDevice", }, + ], + "capped_terminal_exits": [ { - "wire_uuid": "wire-c", - "start_found": False, - "start_terminal_uuid": "terminal-missing-both-a", - "end_found": False, - "end_terminal_uuid": "terminal-missing-both-b", + "name": capped_terminal.Name, + "label": "B1", + "terminal_uuid": "terminal-capped", + "parent_device_name": capped_device.Name, + "parent_device_label": "长度截断设备", }, - ] + { + "name": "MissingCappedTerminal", + "terminal_uuid": "terminal-missing-capped", + "parent_device_name": "MissingCappedDevice", + }, + ], + "invalid_terminal_exit_directions": [ + { + "name": invalid_terminal.Name, + "label": "C1", + "terminal_uuid": "terminal-invalid-direction", + "parent_device_name": invalid_device.Name, + "parent_device_label": "方向无效设备", + }, + { + "name": "MissingInvalidDirectionTerminal", + "terminal_uuid": "terminal-missing-invalid-direction", + "parent_device_name": "MissingInvalidDirectionDevice", + }, + ], + "invalid_terminal_local_routes": [ + { + "name": invalid_local_terminal.Name, + "label": "D1", + "terminal_uuid": "terminal-invalid-local-route", + "parent_device_name": invalid_local_device.Name, + "parent_device_label": "局部路径无效设备", + }, + { + "name": "MissingInvalidLocalRouteTerminal", + "terminal_uuid": "terminal-missing-invalid-local-route", + "parent_device_name": "MissingInvalidLocalRouteDevice", + }, + ], }, ensure_ascii=False, ) @@ -4777,54 +5268,118 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_missing_terminal_counterpart_terminals() + result = auto_routing_panel.AutoRoutingController().select_terminal_exit_issue_terminals() - self.assertEqual(2, result["selected_missing_terminal_counterpart_terminals"]) - self.assertEqual(["TerminalFoundEnd", "TerminalFoundStart"], result["selected_missing_terminal_counterpart_terminal_names"]) - self.assertEqual(["terminal-missing-both-a", "terminal-missing-both-b"], result["missing_terminal_counterpart_refs"]) - self.assertEqual([end_terminal, start_terminal], selected) + self.assertEqual(8, result["selected_terminal_exit_issue_objects"]) + self.assertEqual(4, result["selected_terminal_exit_issue_terminals"]) + self.assertEqual(4, result["selected_terminal_exit_issue_devices"]) + self.assertEqual(1, result["selected_terminal_exit_corrected_terminals"]) + self.assertEqual(1, result["selected_terminal_exit_capped_terminals"]) + self.assertEqual(1, result["selected_terminal_exit_invalid_direction_terminals"]) + self.assertEqual(1, result["selected_terminal_exit_invalid_local_route_terminals"]) + self.assertEqual( + [corrected_terminal.Name, capped_terminal.Name, invalid_terminal.Name, invalid_local_terminal.Name], + result["selected_terminal_exit_issue_terminal_names"], + ) + self.assertEqual( + [corrected_device.Name, capped_device.Name, invalid_device.Name, invalid_local_device.Name], + result["selected_terminal_exit_issue_device_names"], + ) + self.assertEqual( + [ + "MissingCorrectedTerminal", + "MissingCorrectedDevice", + "MissingCappedTerminal", + "MissingCappedDevice", + "MissingInvalidDirectionTerminal", + "MissingInvalidDirectionDevice", + "MissingInvalidLocalRouteTerminal", + "MissingInvalidLocalRouteDevice", + ], + result["missing_terminal_exit_issue_refs"], + ) + self.assertEqual( + [ + corrected_terminal, + corrected_device, + capped_terminal, + capped_device, + invalid_terminal, + invalid_device, + invalid_local_terminal, + invalid_local_device, + ], + selected, + ) - def test_controller_selects_missing_terminal_candidate_terminals_from_latest_batch_diagnostic(self): + def test_route_issue_codes_include_terminal_access_fallback_targets(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + route_data = { + "network": { + "start_terminal_access_target_kind": "RoutingRange", + "end_terminal_access_target_kind": "WireDuct", + }, + "route_track": {"segments": []}, + } + + issue_codes = auto_routing._route_issue_codes(route_data, []) + + self.assertIn("terminal_access_fallback_targets", issue_codes) + + def test_controller_selects_main_path_detour_bridge_endpoint_sources_from_summary(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - candidate_a1 = _terminal(doc, terminal_objects, "TerminalA1", "terminal-a1", app.Vector(0, 0, 0)) - candidate_a2 = _terminal(doc, terminal_objects, "TerminalA2", "terminal-a2", app.Vector(10, 0, 0)) - found_b1 = _terminal(doc, terminal_objects, "TerminalB1", "terminal-b1", app.Vector(20, 0, 0)) - for terminal in (candidate_a1, candidate_a2): - terminal_objects.ensure_string_property(terminal, "QetElementUuid", "QET Exchange", "", "device-a") - terminal_objects.ensure_string_property(terminal, "QetInstanceId", "QET Exchange", "", "instance-a") - diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") - diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") - diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" - diagnostic.QetProjectUuid = "project-1" - diagnostic.QetDiagnosticJson = json.dumps( + wiring_objects.ensure_diagnostic_group(doc, "project-1") + routed_group = wiring_objects.ensure_routed_group(doc, "project-1") + fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") + fallback_source.QetRouteSourceLabel = "安装板布线面" + current_source = doc.addObject("Part::Feature", "MainWireDuctSource") + current_source.QetRouteSourceLabel = "主线槽A" + wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_pair") + wire.Label = "N-PAIR: A1 -> B1" + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-main-path-pair" + wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + wire.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + "QetRouteTrackJson", + ] + wire.QetStartTerminalUuid = "terminal-a" + wire.QetEndTerminalUuid = "terminal-b" + wire.QetRouteDiagnosticsJson = json.dumps( { - "missing_endpoint_samples": [ + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["RoutingRange"], + "rejected_fallback_labels": ["安装板布线面"], + } + }, + ensure_ascii=False, + ) + wire.QetRouteTrackJson = json.dumps( + { + "segments": [ { - "wire_uuid": "wire-a", - "start_found": False, - "start_terminal_uuid": "terminal-missing-a", - "start_instance_id": "instance-a", - "start_element_uuid": "device-a", - "start_missing_endpoint_reason_code": "terminal_uuid_not_in_element", - "start_instance_terminal_samples": [ - {"name": "TerminalA1", "terminal_uuid": "terminal-a1"}, - {"name": "TerminalA2", "terminal_uuid": "terminal-a2"}, - ], - "end_found": True, - "end_terminal_uuid": "terminal-b1", + "carrier": { + "kind": "WireDuct", + "source_label": "主线槽A", + "source_name": current_source.Name, + } } ] }, ensure_ascii=False, ) - diagnostic_group.addObject(diagnostic) + routed_group.addObject(wire) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), @@ -4833,66 +5388,54 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_missing_terminal_candidate_terminals() + result = auto_routing_panel.AutoRoutingController().select_main_path_detour_rejected_fallback_sources() - self.assertEqual(2, result["selected_missing_terminal_candidate_terminals"]) - self.assertEqual(["TerminalA1", "TerminalA2"], result["selected_missing_terminal_candidate_terminal_names"]) - self.assertEqual([], result["missing_terminal_candidate_terminal_refs"]) - self.assertEqual([candidate_a1, candidate_a2], selected) - self.assertNotIn(found_b1, selected) + self.assertEqual(2, result["selected_main_path_detour_bridge_endpoint_objects"]) + self.assertEqual(1, result["selected_main_path_detour_rejected_fallback_sources"]) + self.assertEqual(1, result["selected_main_path_detour_current_route_sources"]) + self.assertEqual([fallback_source.Name], result["selected_main_path_detour_rejected_fallback_source_names"]) + self.assertEqual([current_source.Name], result["selected_main_path_detour_current_route_source_names"]) + self.assertEqual({"主线槽A": 1}, result["main_path_detour_current_route_source_label_counts"]) + self.assertEqual({"安装板布线面 -> 主线槽A": 1}, result["main_path_detour_bridge_pair_counts"]) + self.assertEqual([], result["missing_main_path_detour_current_route_refs"]) + self.assertEqual([fallback_source, current_source], selected) - def test_controller_selects_boundary_issue_route_carriers_and_terminals(self): + def test_controller_selects_issue_wires_from_wire_object_issue_codes(self): _install_fake_freecad() - terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(140, 0, 20)], - label="柜内主路径A", - project_uuid="project-1", - kind="UserPath", - ) - terminal = _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) + sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled") + sampled_wire.Label = "N-SAMPLED: A1 -> B1 (BoundaryWarning)" + sampled_wire.RouteType = "RoutedConnection" + sampled_wire.QetWireUuid = "wire-sampled" + hidden_issue_wire = doc.addObject("Part::Feature", "QETRoutedConnection_hidden") + hidden_issue_wire.Label = "N-HIDDEN: A2 -> B2 (LongAccessWarning)" + hidden_issue_wire.RouteType = "RoutedConnection" + hidden_issue_wire.QetWireUuid = "wire-hidden" + hidden_issue_wire.QetRouteIssueCodes = "long_terminal_access" + normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") + normal_wire.Label = "N-OK: A3 -> B3 (Routed)" + normal_wire.RouteType = "RoutedConnection" + normal_wire.QetWireUuid = "wire-ok" + normal_wire.QetRouteIssueCodes = "" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") - diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") - diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { - "route_carriers_outside_boundary": [ - { - "carrier": { - "name": route.Name, - "label": "柜内主路径A", - }, - "outside_point_count": 1, - }, - { - "carrier": { - "name": "MissingRouteCarrier", - "label": "缺失路径", - }, - "outside_point_count": 1, - }, - ], - "terminals_outside_boundary": [ - { - "name": "TerminalOutside", - "label": "TerminalOutside", - "terminal_uuid": "terminal-outside", - "outside_point_count": 2, - }, + "route_samples": [ { - "name": "MissingTerminal", - "terminal_uuid": "terminal-missing", - "outside_point_count": 1, - }, - ], + "wire_uuid": "wire-sampled", + "wire_object_label": "N-SAMPLED: A1 -> B1 (BoundaryWarning)", + "issue_codes": ["boundary_warning"], + } + ] }, ensure_ascii=False, ) @@ -4905,341 +5448,570 @@ class AutoRoutingTest(unittest.TestCase): getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().select_boundary_issue_route_carriers_and_terminals() + result = auto_routing_panel.AutoRoutingController().select_issue_wires() - self.assertEqual(1, result["selected_boundary_route_carriers"]) - self.assertEqual(1, result["selected_boundary_terminals"]) - self.assertEqual([route.Name], result["selected_boundary_route_carrier_names"]) - self.assertEqual(["TerminalOutside"], result["selected_boundary_terminal_names"]) - self.assertEqual(["MissingRouteCarrier"], result["missing_boundary_route_carrier_refs"]) - self.assertEqual(["MissingTerminal"], result["missing_boundary_terminal_refs"]) - self.assertEqual([route, terminal], selected) + self.assertEqual(2, result["selected_issue_wires"]) + self.assertEqual( + ["QETRoutedConnection_sampled", "QETRoutedConnection_hidden"], + result["selected_issue_wire_names"], + ) + self.assertEqual([], result["missing_issue_wire_refs"]) + self.assertEqual([sampled_wire, hidden_issue_wire], selected) - def test_controller_marks_selected_route_carrier_constraint_modes(self): + def test_controller_selects_long_terminal_accesses_from_latest_batch_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - carrier = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="测试路径", - project_uuid="project-1", - kind="UserPath", - ) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=carrier)], - ) - controller = auto_routing_panel.AutoRoutingController() - - forbidden = controller.mark_selected_route_carriers_forbidden() - required = controller.mark_selected_route_carriers_required() - cleared = controller.clear_selected_route_carrier_constraints() - - self.assertEqual(1, forbidden["route_constraint_carriers"]) - self.assertEqual(1, required["route_constraint_carriers"]) - self.assertEqual(1, cleared["route_constraint_carriers"]) - self.assertEqual("", carrier.QetRouteConstraintMode) + terminal_a = _terminal(doc, terminal_objects, "Terminal325", "terminal-325", app.Vector(0, 0, 0)) + terminal_b = _terminal(doc, terminal_objects, "Terminal326", "terminal-326", app.Vector(10, 0, 0)) + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "routing_path_network_diagnostic": { + "long_terminal_accesses": [ + {"terminal_uuid": "terminal-325", "name": "Terminal325", "label": "325"}, + {"terminal_uuid": "terminal-326", "name": "Terminal326", "label": "326"}, + {"terminal_uuid": "terminal-404", "name": "Terminal404", "label": "404"}, + ] + } + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], + ) - def test_controller_sets_selected_route_carrier_capacity(self): + result = auto_routing_panel.AutoRoutingController().select_long_terminal_accesses() + + self.assertEqual(2, result["selected_long_terminal_accesses"]) + self.assertEqual(["Terminal325", "Terminal326"], result["selected_long_terminal_names"]) + self.assertEqual(["terminal-404"], result["missing_long_terminal_refs"]) + self.assertEqual([terminal_a, terminal_b], selected) + + def test_controller_selects_long_terminal_accesses_from_path_network_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - source = doc.addObject("Sketcher::SketchObject", "CapacityPathSketch") - source.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], - ) - carrier = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="测试路径", - project_uuid="project-1", - kind="UserPath", + terminal_a = _terminal(doc, terminal_objects, "TerminalPathLongA", "terminal-path-a", app.Vector(0, 0, 0)) + terminal_b = _terminal(doc, terminal_objects, "TerminalPathLongB", "terminal-path-b", app.Vector(10, 0, 0)) + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "long_terminal_accesses": [ + {"terminal_uuid": "terminal-path-a", "name": "TerminalPathLongA", "label": "A"}, + {"terminal_uuid": "terminal-path-b", "name": "TerminalPathLongB", "label": "B"}, + {"terminal_uuid": "terminal-path-missing", "name": "TerminalPathMissing"}, + ] + }, + ensure_ascii=False, ) - routing_network._mark_user_path_source(source, carrier) + diagnostic_group.addObject(diagnostic) + selected = [] gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=source)], + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - controller = auto_routing_panel.AutoRoutingController(options={"selected_route_capacity": 5}) - report = controller.set_selected_route_carriers_capacity() + result = auto_routing_panel.AutoRoutingController().select_long_terminal_accesses() - self.assertEqual(1, report["route_capacity_carriers"]) - self.assertEqual(1, report["route_capacity_sources"]) - self.assertEqual(5, source.QetRouteCarrierCapacity) - self.assertEqual(5, carrier.QetRouteCarrierCapacity) + self.assertEqual(2, result["selected_long_terminal_accesses"]) + self.assertEqual(["TerminalPathLongA", "TerminalPathLongB"], result["selected_long_terminal_names"]) + self.assertEqual(["terminal-path-missing"], result["missing_long_terminal_refs"]) + self.assertEqual([terminal_a, terminal_b], selected) - def test_controller_reports_selected_source_route_constraint_before_carrier_generation(self): + def test_controller_selects_long_terminal_access_devices_from_latest_batch_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "FutureUserRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + device_pen = doc.addObject("App::DocumentObjectGroup", "DevicePEN") + device_pen.Label = "PEN" + device_pe = doc.addObject("App::DocumentObjectGroup", "DevicePE") + device_pe.Label = "PE" + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "routing_path_network_diagnostic": { + "long_terminal_accesses": [ + { + "terminal_uuid": "terminal-325", + "parent_device_name": "DevicePEN", + "parent_device_label": "PEN", + }, + { + "terminal_uuid": "terminal-326", + "parent_device_name": "DevicePEN", + "parent_device_label": "PEN", + }, + { + "terminal_uuid": "terminal-327", + "parent_device_label": "PE", + }, + { + "terminal_uuid": "terminal-404", + "parent_device_name": "Device404", + "parent_device_label": "404", + }, + ] + } + }, + ensure_ascii=False, ) + diagnostic_group.addObject(diagnostic) + selected = [] gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - report = auto_routing_panel.AutoRoutingController().mark_selected_route_carriers_required() + result = auto_routing_panel.AutoRoutingController().select_long_terminal_access_devices() - self.assertEqual(0, report["route_constraint_carriers"]) - self.assertEqual(1, report["route_constraint_sources"]) - self.assertEqual("Required", route_path.QetRouteConstraintMode) + self.assertEqual(2, result["selected_long_terminal_access_devices"]) + self.assertEqual(["DevicePEN", "DevicePE"], result["selected_long_terminal_access_device_names"]) + self.assertEqual(["Device404"], result["missing_long_terminal_access_device_refs"]) + self.assertEqual([device_pen, device_pe], selected) - def test_controller_clears_all_route_carrier_constraint_modes(self): + def test_controller_selects_long_terminal_access_devices_from_path_network_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - required = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="必经路径", - project_uuid="project-1", - kind="UserPath", + device_a = doc.addObject("App::DocumentObjectGroup", "DevicePathLongA") + device_a.Label = "长接入设备A" + device_b = doc.addObject("App::DocumentObjectGroup", "DevicePathLongB") + device_b.Label = "长接入设备B" + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "long_terminal_accesses": [ + { + "terminal_uuid": "terminal-path-a", + "parent_device_name": "DevicePathLongA", + "parent_device_label": "长接入设备A", + }, + { + "terminal_uuid": "terminal-path-b", + "parent_device_label": "长接入设备B", + }, + { + "terminal_uuid": "terminal-path-missing", + "parent_device_name": "DevicePathMissing", + }, + ] + }, + ensure_ascii=False, ) - forbidden = routing_network.create_route_carrier( - doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="禁经路径", - project_uuid="project-1", - kind="UserPath", + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - required.QetRouteConstraintMode = "Required" - forbidden.QetRouteConstraintMode = "Forbidden" - controller = auto_routing_panel.AutoRoutingController() - report = controller.clear_all_route_carrier_constraints() + result = auto_routing_panel.AutoRoutingController().select_long_terminal_access_devices() - self.assertEqual(2, report["route_constraint_carriers"]) - self.assertEqual("", required.QetRouteConstraintMode) - self.assertEqual("", forbidden.QetRouteConstraintMode) - self.assertNotIn("路径约束", controller.summary()) + self.assertEqual(2, result["selected_long_terminal_access_devices"]) + self.assertEqual(["DevicePathLongA", "DevicePathLongB"], result["selected_long_terminal_access_device_names"]) + self.assertEqual(["DevicePathMissing"], result["missing_long_terminal_access_device_refs"]) + self.assertEqual([device_a, device_b], selected) - def test_selected_source_route_constraint_survives_carrier_regeneration(self): + def test_controller_selects_missing_terminal_devices_from_latest_batch_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "UserRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + device_a = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") + terminal_objects.ensure_string_property(device_a, "QetElementUuid", "QET Exchange", "", "device-a") + terminal_objects.ensure_string_property(device_a, "QetInstanceId", "QET Exchange", "", "instance-a") + device_b = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_b") + terminal_objects.ensure_string_property(device_b, "QetElementUuid", "QET Exchange", "", "device-b") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-missing-a", + "start_instance_id": "instance-a", + "start_element_uuid": "device-a", + "end_found": True, + }, + { + "wire_uuid": "wire-b", + "start_found": False, + "start_terminal_uuid": "terminal-missing-b", + "start_instance_id": "instance-missing", + "start_element_uuid": "device-missing", + "end_found": False, + "end_terminal_uuid": "terminal-missing-c", + "end_element_uuid": "device-b", + }, + ] + }, + ensure_ascii=False, ) - selection = [FakeSelectionItem(obj=route_path)] - first = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - routing_network.clear_route_carriers(doc) - second = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", - ) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() - self.assertEqual(1, len(first)) - self.assertEqual(1, len(second)) - self.assertEqual("Required", route_path.QetRouteConstraintMode) - self.assertEqual("Required", second[0].QetRouteConstraintMode) + self.assertEqual(2, result["selected_missing_terminal_devices"]) + self.assertEqual(["QETDevice_device_a", "QETDevice_device_b"], result["selected_missing_terminal_device_names"]) + self.assertEqual(["terminal-missing-b"], result["missing_terminal_device_refs"]) + self.assertEqual([device_a, device_b], selected) - def test_refreshing_user_path_clears_stale_constraint_when_source_is_cleared(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "UserRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], - ) - selection = [FakeSelectionItem(obj=route_path)] - carriers = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", - ) - route_path.QetRouteConstraintMode = "" - carriers[0].QetRouteConstraintMode = "Required" - - refreshed = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", - ) - - self.assertEqual(1, len(refreshed)) - self.assertEqual("", refreshed[0].QetRouteConstraintMode) - - def test_selected_multi_wire_source_route_constraint_marks_all_user_paths(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 120, 0, 80, 20, 20), - wires=[ - FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), - FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), - ], - ) - selection = [FakeSelectionItem(obj=route_path)] - carriers = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", - ) - - marked = routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - - self.assertEqual(2, len(carriers)) - self.assertEqual(2, len(marked)) - self.assertEqual("Required", route_path.QetRouteConstraintMode) - self.assertEqual(["Required", "Required"], [carrier.QetRouteConstraintMode for carrier in carriers]) - - def test_controller_clears_selected_multi_wire_source_route_constraints(self): + def test_controller_selects_missing_terminal_device_by_device_label_fallback(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 120, 0, 80, 20, 20), - wires=[ - FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), - FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), - ], - ) - selection = [FakeSelectionItem(obj=route_path)] - carriers = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_no_uuid") + device.Label = "缺端子设备A" + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-missing-a", + "start_device_label": "缺端子设备A", + "end_found": True, + } + ] + }, + ensure_ascii=False, ) - routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") + diagnostic_group.addObject(diagnostic) + selected = [] gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: selection, + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - cleared = auto_routing_panel.AutoRoutingController().clear_selected_route_carrier_constraints() + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() - self.assertEqual(2, cleared["route_constraint_carriers"]) - self.assertEqual("", route_path.QetRouteConstraintMode) - self.assertEqual(["", ""], [carrier.QetRouteConstraintMode for carrier in carriers]) + self.assertEqual(1, result["selected_missing_terminal_devices"]) + self.assertEqual(["QETDevice_no_uuid"], result["selected_missing_terminal_device_names"]) + self.assertEqual([], result["missing_terminal_device_refs"]) + self.assertEqual([device], selected) - def test_clear_all_route_constraints_clears_source_objects_too(self): + def test_controller_reports_missing_terminal_device_reason_counts_when_device_not_in_scene(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "UserRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-missing-a", + "start_element_uuid": "device-a", + "start_instance_id": "instance-a", + "start_device_label": "UD:8", + "start_terminal_display": "as", + "start_missing_endpoint_reason_code": "device_not_in_3d_scene", + "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", + "end_found": True, + } + ] + }, + ensure_ascii=False, ) - selection = [FakeSelectionItem(obj=route_path)] - carriers = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - report = routing_network.clear_all_route_constraint_modes(doc) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() - self.assertEqual(1, report["route_constraint_carriers"]) - self.assertEqual(1, report["route_constraint_sources"]) - self.assertEqual("", route_path.QetRouteConstraintMode) - self.assertEqual("", carriers[0].QetRouteConstraintMode) + self.assertEqual(0, result["selected_missing_terminal_devices"]) + self.assertEqual(["terminal-missing-a"], result["missing_terminal_device_refs"]) + self.assertEqual(["UD:8"], result["missing_terminal_device_labels"]) + self.assertEqual(["instance-a"], result["missing_terminal_device_instance_ids"]) + self.assertEqual(["device-a"], result["missing_terminal_device_element_uuids"]) + self.assertEqual({"device_not_in_3d_scene": 1}, result["missing_terminal_device_reason_counts"]) + self.assertEqual([], selected) - def test_selected_points_object_can_be_used_as_user_path(self): + def test_controller_selects_found_counterpart_terminals_from_missing_endpoint_samples(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "PointRoute") - route_path.Points = [ - app.Vector(0, 0, 20), - app.Vector(40, 0, 20), - app.Vector(40, 30, 20), - ] + end_terminal = _terminal(doc, terminal_objects, "TerminalFoundEnd", "terminal-found-end", app.Vector(20, 0, 0)) + start_terminal = _terminal( + doc, + terminal_objects, + "TerminalFoundStart", + "terminal-found-start", + app.Vector(40, 0, 0), + ) + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "wire_label": "F6", + "start_found": False, + "start_terminal_uuid": "terminal-missing-start", + "end_found": True, + "end_terminal_uuid": "terminal-found-end", + "end_terminal_display": "6", + }, + { + "wire_uuid": "wire-b", + "wire_label": "N2", + "start_found": True, + "start_terminal_uuid": "terminal-found-start", + "start_terminal_display": "1", + "end_found": False, + "end_terminal_uuid": "terminal-missing-end", + }, + { + "wire_uuid": "wire-c", + "start_found": False, + "start_terminal_uuid": "terminal-missing-both-a", + "end_found": False, + "end_terminal_uuid": "terminal-missing-both-b", + }, + ] + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + selected = [] gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carriers = routing_network.collect_route_carriers(doc) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_counterpart_terminals() - self.assertEqual(1, result["user_path_carriers"]) - self.assertEqual( - [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0), (40.0, 30.0, 20.0)], - [(point.x, point.y, point.z) for point in carriers[0].Points], - ) + self.assertEqual(2, result["selected_missing_terminal_counterpart_terminals"]) + self.assertEqual(["TerminalFoundEnd", "TerminalFoundStart"], result["selected_missing_terminal_counterpart_terminal_names"]) + self.assertEqual(["terminal-missing-both-a", "terminal-missing-both-b"], result["missing_terminal_counterpart_refs"]) + self.assertEqual([end_terminal, start_terminal], selected) - def test_selected_user_path_copies_source_capacity(self): + def test_controller_selects_missing_terminal_candidate_terminals_from_latest_batch_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "PointRoute") - route_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] - route_path.QetRouteCarrierCapacity = 5 - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], - ) - - auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carrier = routing_network.collect_route_carriers(doc)[0] + candidate_a1 = _terminal(doc, terminal_objects, "TerminalA1", "terminal-a1", app.Vector(0, 0, 0)) + candidate_a2 = _terminal(doc, terminal_objects, "TerminalA2", "terminal-a2", app.Vector(10, 0, 0)) + found_b1 = _terminal(doc, terminal_objects, "TerminalB1", "terminal-b1", app.Vector(20, 0, 0)) + for terminal in (candidate_a1, candidate_a2): + terminal_objects.ensure_string_property(terminal, "QetElementUuid", "QET Exchange", "", "device-a") + terminal_objects.ensure_string_property(terminal, "QetInstanceId", "QET Exchange", "", "instance-a") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-missing-a", + "start_instance_id": "instance-a", + "start_element_uuid": "device-a", + "start_missing_endpoint_reason_code": "terminal_uuid_not_in_element", + "start_instance_terminal_samples": [ + {"name": "TerminalA1", "terminal_uuid": "terminal-a1"}, + {"name": "TerminalA2", "terminal_uuid": "terminal-a2"}, + ], + "end_found": True, + "end_terminal_uuid": "terminal-b1", + } + ] + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], + ) - self.assertEqual(5, carrier.QetRouteCarrierCapacity) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_candidate_terminals() - def test_selected_multi_wire_user_path_copies_source_capacity_to_all_carriers(self): + self.assertEqual(2, result["selected_missing_terminal_candidate_terminals"]) + self.assertEqual(["TerminalA1", "TerminalA2"], result["selected_missing_terminal_candidate_terminal_names"]) + self.assertEqual([], result["missing_terminal_candidate_terminal_refs"]) + self.assertEqual([candidate_a1, candidate_a2], selected) + self.assertNotIn(found_b1, selected) + + def test_controller_selects_boundary_issue_route_carriers_and_terminals(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + route = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(140, 0, 20)], + label="柜内主路径A", + project_uuid="project-1", + kind="UserPath", + ) + terminal = _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "route_carriers_outside_boundary": [ + { + "carrier": { + "name": route.Name, + "label": "柜内主路径A", + }, + "outside_point_count": 1, + }, + { + "carrier": { + "name": "MissingRouteCarrier", + "label": "缺失路径", + }, + "outside_point_count": 1, + }, + ], + "terminals_outside_boundary": [ + { + "name": "TerminalOutside", + "label": "TerminalOutside", + "terminal_uuid": "terminal-outside", + "outside_point_count": 2, + }, + { + "name": "MissingTerminal", + "terminal_uuid": "terminal-missing", + "outside_point_count": 1, + }, + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], + ) + + result = auto_routing_panel.AutoRoutingController().select_boundary_issue_route_carriers_and_terminals() + + self.assertEqual(1, result["selected_boundary_route_carriers"]) + self.assertEqual(1, result["selected_boundary_terminals"]) + self.assertEqual([route.Name], result["selected_boundary_route_carrier_names"]) + self.assertEqual(["TerminalOutside"], result["selected_boundary_terminal_names"]) + self.assertEqual(["MissingRouteCarrier"], result["missing_boundary_route_carrier_refs"]) + self.assertEqual(["MissingTerminal"], result["missing_boundary_terminal_refs"]) + self.assertEqual([route, terminal], selected) + + def test_controller_marks_selected_route_carrier_constraint_modes(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -5248,27 +6020,29 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "CapacityMultiWireRouteSketch") - route_path.QetRouteCarrierCapacity = 4 - route_path.Shape = FakeShape( - FakeBoundBox(0, 120, 0, 80, 20, 20), - wires=[ - FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), - FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), - ], + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="测试路径", + project_uuid="project-1", + kind="UserPath", ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + getSelectionEx=lambda: [FakeSelectionItem(obj=carrier)], ) + controller = auto_routing_panel.AutoRoutingController() - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carriers = routing_network.collect_route_carriers(doc) + forbidden = controller.mark_selected_route_carriers_forbidden() + required = controller.mark_selected_route_carriers_required() + cleared = controller.clear_selected_route_carrier_constraints() - self.assertEqual(2, result["user_path_carriers"]) - self.assertEqual([4, 4], [carrier.QetRouteCarrierCapacity for carrier in carriers]) + self.assertEqual(1, forbidden["route_constraint_carriers"]) + self.assertEqual(1, required["route_constraint_carriers"]) + self.assertEqual(1, cleared["route_constraint_carriers"]) + self.assertEqual("", carrier.QetRouteConstraintMode) - def test_selected_user_path_projects_line_to_selected_face(self): + def test_controller_sets_selected_route_carrier_capacity(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -5277,39 +6051,42 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - face = FakeFace( - FakeBoundBox(0, 100, 0, 100, 0, 0), - app.Vector(0, 0, 1), + source = doc.addObject("Sketcher::SketchObject", "CapacityPathSketch") + source.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) - draft_line = doc.addObject("Part::Feature", "FloatingDraftLine") - draft_line.Shape = FakeShape( - FakeBoundBox(10, 90, 10, 90, 25, 35), - edges=[FakeEdge(app.Vector(10, 10, 25), app.Vector(90, 90, 35))], + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="测试路径", + project_uuid="project-1", + kind="UserPath", ) + routing_network._mark_user_path_source(source, carrier) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], - getSelectionEx=lambda: [ - FakeSelectionItem([face]), - FakeSelectionItem(obj=draft_line), - ], + getSelectionEx=lambda: [FakeSelectionItem(obj=source)], ) + controller = auto_routing_panel.AutoRoutingController(options={"selected_route_capacity": 5}) - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carriers = routing_network.collect_route_carriers(doc) + report = controller.set_selected_route_carriers_capacity() - self.assertEqual(1, result["user_path_carriers"]) - self.assertEqual([2.0, 2.0], [point.z for point in carriers[0].Points]) + self.assertEqual(1, report["route_capacity_carriers"]) + self.assertEqual(1, report["route_capacity_sources"]) + self.assertEqual(5, source.QetRouteCarrierCapacity) + self.assertEqual(5, carrier.QetRouteCarrierCapacity) - def test_controller_create_user_paths_reports_removed_stale_source_carriers(self): + def test_controller_reports_selected_source_route_constraint_before_carrier_generation(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path = doc.addObject("Sketcher::SketchObject", "FutureUserRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], @@ -5318,143 +6095,135 @@ class AutoRoutingTest(unittest.TestCase): getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - doc.removeObject("UserRouteSketch") - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [], - ) - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + report = auto_routing_panel.AutoRoutingController().mark_selected_route_carriers_required() - self.assertEqual(1, result["removed_stale_carriers"]) - self.assertEqual(0, result["network"]["carriers"]) - self.assertEqual([], routing_network.collect_route_carriers(doc)) + self.assertEqual(0, report["route_constraint_carriers"]) + self.assertEqual(1, report["route_constraint_sources"]) + self.assertEqual("Required", route_path.QetRouteConstraintMode) - def test_terminal_access_uses_terminal_local_route_points_before_main_network(self): + def test_controller_clears_all_route_carrier_constraint_modes(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") - terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [10, 0, 0], [10, 30, 0]]) - routing_network.create_route_carrier( + required = routing_network.create_route_carrier( doc, - [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="必经路径", project_uuid="project-1", kind="UserPath", ) - - created = routing_network.create_terminal_access_carriers_from_document( + forbidden = routing_network.create_route_carrier( doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="禁经路径", project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=100.0, + kind="UserPath", ) + required.QetRouteConstraintMode = "Required" + forbidden.QetRouteConstraintMode = "Forbidden" + controller = auto_routing_panel.AutoRoutingController() - self.assertEqual(1, len(created)) - self.assertEqual( - [(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 30.0, 0.0)], - [(p.x, p.y, p.z) for p in created[0].Points[:3]], - ) + report = controller.clear_all_route_carrier_constraints() - def test_terminal_access_accepts_object_wrapped_local_route_points(self): + self.assertEqual(2, report["route_constraint_carriers"]) + self.assertEqual("", required.QetRouteConstraintMode) + self.assertEqual("", forbidden.QetRouteConstraintMode) + self.assertNotIn("路径约束", controller.summary()) + + def test_selected_source_route_constraint_survives_carrier_regeneration(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") - terminal.QetTerminalLocalRoutePointsJson = json.dumps( - { - "points": [ - {"x": 0, "y": 0, "z": 0}, - {"x": 10, "y": 0, "z": 0}, - {"x": 10, "y": 30, "z": 0}, - ] - } - ) - routing_network.create_route_carrier( - doc, - [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], + route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + ) + selection = [FakeSelectionItem(obj=route_path)] + first = routing_network.create_user_path_carriers_from_selection( + doc, + selection, project_uuid="project-1", - kind="UserPath", ) + routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") + routing_network.clear_route_carriers(doc) - created = routing_network.create_terminal_access_carriers_from_document( + second = routing_network.create_user_path_carriers_from_selection( doc, + selection, project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=100.0, ) - self.assertEqual(1, len(created)) - self.assertEqual( - [(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 30.0, 0.0)], - [(p.x, p.y, p.z) for p in created[0].Points[:3]], - ) + self.assertEqual(1, len(first)) + self.assertEqual(1, len(second)) + self.assertEqual("Required", route_path.QetRouteConstraintMode) + self.assertEqual("Required", second[0].QetRouteConstraintMode) - def test_controller_sets_selected_terminal_local_route_from_selected_path(self): + def test_refreshing_user_path_clears_stale_constraint_when_source_is_cleared(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] - gui = sys.modules["FreeCADGui"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(100, 10, 0)) - route_path = doc.addObject("Part::Feature", "LocalExitSketch") + route_path = doc.addObject("Part::Feature", "UserRouteSketch") route_path.Shape = FakeShape( - FakeBoundBox(100, 130, 10, 40, 0, 0), - edges=[ - FakeEdge(app.Vector(100, 10, 0), app.Vector(130, 10, 0)), - FakeEdge(app.Vector(130, 10, 0), app.Vector(130, 40, 0)), - ], + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [ - FakeSelectionItem(obj=terminal), - FakeSelectionItem(obj=route_path), - ], + selection = [FakeSelectionItem(obj=route_path)] + carriers = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", ) + route_path.QetRouteConstraintMode = "" + carriers[0].QetRouteConstraintMode = "Required" - result = auto_routing_panel.AutoRoutingController().set_selected_terminal_local_route_points() - points = json.loads(terminal.QetTerminalLocalRoutePointsJson) - access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0) - - self.assertEqual(1, result["terminal_local_routes"]) - self.assertEqual("TerminalStart", result["terminal_local_route_names"][0]) - self.assertEqual( - [[0.0, 0.0, 0.0], [30.0, 0.0, 0.0], [30.0, 30.0, 0.0]], - points, - ) - self.assertEqual( - [(100.0, 10.0, 0.0), (130.0, 10.0, 0.0), (130.0, 40.0, 0.0)], - [(point.x, point.y, point.z) for point in access_points], + refreshed = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", ) - def test_terminal_access_extends_past_parent_device_bbox_when_no_local_route_exists(self): + self.assertEqual(1, len(refreshed)) + self.assertEqual("", refreshed[0].QetRouteConstraintMode) + + def test_selected_multi_wire_source_route_constraint_marks_all_user_paths(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - device = doc.addObject("App::DocumentObjectGroup", "ProtectionDevice") - device.Shape = FakeShape(FakeBoundBox(-20, 20, -20, 20, -5, 60)) - terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - device.addObject(terminal) + route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), + FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), + ], + ) + selection = [FakeSelectionItem(obj=route_path)] + carriers = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", + ) - access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0) + marked = routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - self.assertEqual((0.0, 0.0, 0.0), (access_points[0].x, access_points[0].y, access_points[0].z)) - self.assertGreater(access_points[-1].z, 60.0) + self.assertEqual(2, len(carriers)) + self.assertEqual(2, len(marked)) + self.assertEqual("Required", route_path.QetRouteConstraintMode) + self.assertEqual(["Required", "Required"], [carrier.QetRouteConstraintMode for carrier in carriers]) - def test_generate_routing_paths_refreshes_selected_user_path_without_duplicate(self): + def test_controller_clears_selected_multi_wire_source_route_constraints(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -5463,54 +6232,59 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), + FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), + ], + ) + selection = [FakeSelectionItem(obj=route_path)] + carriers = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", ) + routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") gui.Selection = types.SimpleNamespace( getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + getSelectionEx=lambda: selection, ) - first = auto_routing_panel.AutoRoutingController().generate_routing_paths() - route_path.Shape = FakeShape( - FakeBoundBox(0, 200, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(200, 0, 20))], - ) - second = auto_routing_panel.AutoRoutingController().generate_routing_paths() - user_paths = [ - item - for item in routing_network.collect_route_carriers(doc) - if item.QetRouteCarrierKind == "UserPath" - ] + cleared = auto_routing_panel.AutoRoutingController().clear_selected_route_carrier_constraints() - self.assertEqual(1, first["user_path_carriers"]) - self.assertEqual(1, second["user_path_carriers"]) - self.assertEqual(1, len(user_paths)) - self.assertEqual([(0.0, 0.0, 20.0), (200.0, 0.0, 20.0)], [(p.x, p.y, p.z) for p in user_paths[0].Points]) + self.assertEqual(2, cleared["route_constraint_carriers"]) + self.assertEqual("", route_path.QetRouteConstraintMode) + self.assertEqual(["", ""], [carrier.QetRouteConstraintMode for carrier in carriers]) - def test_eplan_connection_route_can_use_generated_user_path(self): + def test_clear_all_route_constraints_clears_source_objects_too(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(200, 0, 0)) - routing_network.create_route_carrier( + route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + ) + selection = [FakeSelectionItem(obj=route_path)] + carriers = routing_network.create_user_path_carriers_from_selection( doc, - [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], + selection, project_uuid="project-1", - kind="UserPath", ) + routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + report = routing_network.clear_all_route_constraint_modes(doc) - self.assertEqual("Routed", result["route_status"]) - self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + self.assertEqual(1, report["route_constraint_carriers"]) + self.assertEqual(1, report["route_constraint_sources"]) + self.assertEqual("", route_path.QetRouteConstraintMode) + self.assertEqual("", carriers[0].QetRouteConstraintMode) - def test_generate_routing_paths_does_not_duplicate_selected_wire_duct_carriers(self): + def test_selected_points_object_can_be_used_as_user_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -5519,29 +6293,27 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - duct = doc.addObject("Part::Feature", "UnlabeledLongDuct") - duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + route_path = doc.addObject("Part::Feature", "PointRoute") + route_path.Points = [ + app.Vector(0, 0, 20), + app.Vector(40, 0, 20), + app.Vector(40, 30, 20), + ] gui.Selection = types.SimpleNamespace( getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - first = auto_routing_panel.AutoRoutingController().generate_routing_paths() - second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() carriers = routing_network.collect_route_carriers(doc) - self.assertEqual(1, first["selected_wire_duct_carriers"]) - self.assertEqual(0, second["selected_wire_duct_carriers"]) - self.assertEqual( - 1, - len([item for item in carriers if item.QetRouteCarrierKind == "WireDuct"]), - ) + self.assertEqual(1, result["user_path_carriers"]) self.assertEqual( - 2, - len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), + [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0), (40.0, 30.0, 20.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], ) - def test_generate_routing_paths_refreshes_selected_wire_duct_geometry(self): + def test_selected_user_path_copies_source_capacity(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -5550,421 +6322,387 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - duct = doc.addObject("Part::Feature", "UnlabeledLongDuct") - duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + route_path = doc.addObject("Part::Feature", "PointRoute") + route_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] + route_path.QetRouteCarrierCapacity = 5 gui.Selection = types.SimpleNamespace( getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - auto_routing_panel.AutoRoutingController().generate_routing_paths() - duct.Shape = FakeShape(FakeBoundBox(0, 220, -10, 10, 0, 20)) - second = auto_routing_panel.AutoRoutingController().generate_routing_paths() - carriers = routing_network.collect_route_carriers(doc) - main = [item for item in carriers if item.QetRouteCarrierKind == "WireDuct"][0] - open_end_x_values = sorted( - point.x - for item in carriers - if item.QetRouteCarrierKind == "WireDuctOpenEnd" - for point in item.Points - ) + auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carrier = routing_network.collect_route_carriers(doc)[0] - self.assertEqual(0, second["selected_wire_duct_carriers"]) - self.assertEqual([(20.0, 0.0, 10.0), (200.0, 0.0, 10.0)], [(p.x, p.y, p.z) for p in main.Points]) - self.assertEqual([20.0, 20.0, 200.0, 200.0], open_end_x_values) + self.assertEqual(5, carrier.QetRouteCarrierCapacity) - def test_generate_routing_paths_removes_generated_wire_duct_carriers_after_source_deleted(self): + def test_selected_multi_wire_user_path_copies_source_capacity_to_all_carriers(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "Wire Duct A" - duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + route_path = doc.addObject("Sketcher::SketchObject", "CapacityMultiWireRouteSketch") + route_path.QetRouteCarrierCapacity = 4 + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), + FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) - auto_routing_panel.AutoRoutingController().generate_routing_paths() - generated = [ - item - for item in routing_network.collect_route_carriers(doc) - if getattr(item, "QetRouteSourceName", "") == "WireDuctA" - ] - doc.removeObject("WireDuctA") - auto_routing_panel.AutoRoutingController().generate_routing_paths() + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - self.assertEqual(3, len(generated)) - self.assertEqual([], routing_network.collect_route_carriers(doc)) + self.assertEqual(2, result["user_path_carriers"]) + self.assertEqual([4, 4], [carrier.QetRouteCarrierCapacity for carrier in carriers]) - def test_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self): + def test_selected_user_path_projects_line_to_selected_face(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "Mounting Plate A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + face = FakeFace( + FakeBoundBox(0, 100, 0, 100, 0, 0), + app.Vector(0, 0, 1), + ) + draft_line = doc.addObject("Part::Feature", "FloatingDraftLine") + draft_line.Shape = FakeShape( + FakeBoundBox(10, 90, 10, 90, 25, 35), + edges=[FakeEdge(app.Vector(10, 10, 25), app.Vector(90, 90, 35))], + ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=panel)], + getSelectionEx=lambda: [ + FakeSelectionItem([face]), + FakeSelectionItem(obj=draft_line), + ], ) - result = auto_routing_panel.AutoRoutingController().generate_layout_space() - - self.assertGreater(result["support_surface_sources"], 0) - self.assertEqual("document", result["source_mode"]) - - def test_generate_routing_path_network_adds_terminal_access_to_route_network(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") - app = sys.modules["FreeCAD"] - doc = FakeDocument() - app.ActiveDocument = doc - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "Wire Duct A" - duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - - result = auto_routing_panel.AutoRoutingController().generate_routing_paths() - result_again = auto_routing_panel.AutoRoutingController().generate_routing_paths() - access_carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" - ] + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - self.assertEqual(1, result["wire_duct_carriers"]) - self.assertEqual(2, result["wire_duct_open_end_carriers"]) - self.assertEqual(2, result["terminal_access_carriers"]) - self.assertEqual(0, result_again["wire_duct_carriers"]) - self.assertEqual(0, result_again["wire_duct_open_end_carriers"]) - self.assertEqual(2, result_again["terminal_access_carriers"]) - self.assertEqual(2, len(access_carriers)) - self.assertGreater(result["network"]["segments"], 0) + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual([2.0, 2.0], [point.z for point in carriers[0].Points]) - def test_generate_routing_path_network_connects_terminal_access_to_nearest_segment_point(self): + def test_controller_create_user_paths_reports_removed_stale_source_carriers(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalMid", "terminal-mid", app.Vector(50, 30, 0)) - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "Wire Duct A" - duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + doc.removeObject("UserRouteSketch") + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [], + ) - auto_routing_panel.AutoRoutingController().generate_routing_paths() - access_carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" - ] + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - self.assertEqual(1, len(access_carriers)) - end_point = access_carriers[0].Points[-1] - self.assertEqual((50.0, 0.0, 20.0), (end_point.x, end_point.y, end_point.z)) + self.assertEqual(1, result["removed_stale_carriers"]) + self.assertEqual(0, result["network"]["carriers"]) + self.assertEqual([], routing_network.collect_route_carriers(doc)) - def test_terminal_access_prefers_larger_connected_network_over_nearer_isolated_stub(self): + def test_terminal_access_uses_terminal_local_route_points_before_main_network(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 1, 20), app.Vector(5, 1, 20)], - project_uuid="project-1", - kind="WireDuct", - ) + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [10, 0, 0], [10, 30, 0]]) routing_network.create_route_carrier( doc, - [ - app.Vector(0, 10, 20), - app.Vector(40, 10, 20), - app.Vector(80, 10, 20), - app.Vector(120, 10, 20), - ], + [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=100.0, ) self.assertEqual(1, len(created)) - end_point = created[0].Points[-1] - self.assertEqual((0.0, 10.0, 20.0), (end_point.x, end_point.y, end_point.z)) - - def test_connection_entry_candidates_prefer_wire_duct_over_terminal_access(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(0, 10, 20)], - project_uuid="project-1", - kind="TerminalAccess", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - network = routing_network.build_route_graph(doc) - - ranked = routing_network.rank_connection_point_candidates( - network, - routing_network.connection_point_candidates(network, app.Vector(0, 0, 20), limit=0), + self.assertEqual( + [(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 30.0, 0.0)], + [(p.x, p.y, p.z) for p in created[0].Points[:3]], ) - first_kind = getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "") - self.assertEqual("WireDuct", first_kind) - - def test_terminal_access_prefers_wire_duct_over_nearer_routing_range(self): + def test_terminal_access_accepts_object_wrapped_local_route_points(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], - project_uuid="project-1", - kind="RoutingRange", - label="近处布线面", + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps( + { + "points": [ + {"x": 0, "y": 0, "z": 0}, + {"x": 10, "y": 0, "z": 0}, + {"x": 10, "y": 30, "z": 0}, + ] + } ) routing_network.create_route_carrier( doc, - [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], + [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], project_uuid="project-1", - kind="WireDuct", - label="较远线槽", + kind="UserPath", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, - max_distance=1000.0, + max_distance=100.0, ) self.assertEqual(1, len(created)) - end_point = created[0].Points[-1] - self.assertEqual((50.0, 100.0, 20.0), (end_point.x, end_point.y, end_point.z)) + self.assertEqual( + [(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 30.0, 0.0)], + [(p.x, p.y, p.z) for p in created[0].Points[:3]], + ) - def test_terminal_access_prefers_main_path_over_routing_range_in_same_component(self): + def test_controller_sets_selected_terminal_local_route_from_selected_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], - project_uuid="project-1", - kind="RoutingRange", - label="近处布线面", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], - project_uuid="project-1", - kind="WireDuct", - label="较远线槽", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(50, 5, 20), app.Vector(50, 100, 20)], - project_uuid="project-1", - kind="UserPath", - label="线槽接入桥", + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(100, 10, 0)) + route_path = doc.addObject("Part::Feature", "LocalExitSketch") + route_path.Shape = FakeShape( + FakeBoundBox(100, 130, 10, 40, 0, 0), + edges=[ + FakeEdge(app.Vector(100, 10, 0), app.Vector(130, 10, 0)), + FakeEdge(app.Vector(130, 10, 0), app.Vector(130, 40, 0)), + ], ) - network = routing_network.build_route_graph(doc) - ranked = routing_network.rank_connection_point_candidates( - network, - routing_network.connection_point_candidates(network, app.Vector(50, 0, 20), limit=0), + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [ + FakeSelectionItem(obj=terminal), + FakeSelectionItem(obj=route_path), + ], ) - created = routing_network.create_terminal_access_carriers_from_document( - doc, - project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=1000.0, - ) + result = auto_routing_panel.AutoRoutingController().set_selected_terminal_local_route_points() + points = json.loads(terminal.QetTerminalLocalRoutePointsJson) + access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0) - self.assertEqual(1, len(created)) - self.assertEqual("UserPath", getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "")) - end_point = created[0].Points[-1] - self.assertEqual((50.0, 5.0, 20.0), (end_point.x, end_point.y, end_point.z)) + self.assertEqual(1, result["terminal_local_routes"]) + self.assertEqual("TerminalStart", result["terminal_local_route_names"][0]) + self.assertEqual( + [[0.0, 0.0, 0.0], [30.0, 0.0, 0.0], [30.0, 30.0, 0.0]], + points, + ) + self.assertEqual( + [(100.0, 10.0, 0.0), (130.0, 10.0, 0.0), (130.0, 40.0, 0.0)], + [(point.x, point.y, point.z) for point in access_points], + ) - def test_diverse_connection_entry_candidates_keep_multiple_components(self): + def test_controller_sets_selected_terminal_exit_direction_from_selected_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - near = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], - project_uuid="project-1", - kind="WireDuct", - label="近组件", + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(100, 10, 0)) + direction_path = doc.addObject("Part::Feature", "ExitDirectionLine") + direction_path.Shape = FakeShape( + FakeBoundBox(100, 100, 10, 40, 0, 0), + edges=[FakeEdge(app.Vector(100, 10, 0), app.Vector(100, 40, 0))], ) - far = routing_network.create_route_carrier( - doc, - [app.Vector(0, 100, 0), app.Vector(100, 100, 0)], - project_uuid="project-1", - kind="RoutingRange", - label="远组件", - ) - network = routing_network.build_route_graph(doc) - near_key = routing_network._point_key(app.Vector(0, 0, 0)) - far_key = routing_network._point_key(app.Vector(0, 100, 0)) - candidates = [ - { - "key": near_key, - "projected_key": routing_network._point_key(app.Vector(index, 0, 0)), - "point": app.Vector(index, 0, 0), - "distance": index, - "carrier": near, - } - for index in range(1, 6) - ] - candidates.append( - { - "key": far_key, - "projected_key": far_key, - "point": app.Vector(0, 100, 0), - "distance": 100.0, - "carrier": far, - } + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [ + FakeSelectionItem(obj=terminal), + FakeSelectionItem(obj=direction_path), + ], ) - selected = routing_network.select_diverse_connection_point_candidates(network, candidates, limit=3) + result = auto_routing_panel.AutoRoutingController().set_selected_terminal_exit_direction() + direction = json.loads(terminal.QetTerminalExitDirectionJson) + access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0) - self.assertEqual(3, len(selected)) - self.assertIn(far, [candidate.get("carrier") for candidate in selected]) + self.assertEqual(1, result["terminal_exit_directions"]) + self.assertEqual("TerminalStart", result["terminal_exit_direction_names"][0]) + self.assertEqual({"x": 0.0, "y": 1.0, "z": 0.0}, direction) + self.assertEqual((100.0, 30.0, 0.0), (access_points[-1].x, access_points[-1].y, access_points[-1].z)) + self.assertEqual({"x": 0.0, "y": 1.0, "z": 0.0}, result["terminal_exit_direction"]) - def test_terminal_access_prefers_wire_duct_over_nearer_routing_range(self): + def test_terminal_access_extends_past_parent_device_bbox_when_no_local_route_exists(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 1, 20), app.Vector(120, 1, 20)], - project_uuid="project-1", - kind="RoutingRange", + device = doc.addObject("App::DocumentObjectGroup", "ProtectionDevice") + device.Shape = FakeShape(FakeBoundBox(-20, 20, -20, 20, -5, 60)) + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + device.addObject(terminal) + + access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0) + + self.assertEqual((0.0, 0.0, 0.0), (access_points[0].x, access_points[0].y, access_points[0].z)) + self.assertGreater(access_points[-1].z, 60.0) + + def test_generate_routing_paths_refreshes_selected_user_path_without_duplicate(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 10, 20), app.Vector(120, 10, 20)], - project_uuid="project-1", - kind="WireDuct", + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - created = routing_network.create_terminal_access_carriers_from_document( - doc, - project_uuid="project-1", + first = auto_routing_panel.AutoRoutingController().generate_routing_paths() + route_path.Shape = FakeShape( + FakeBoundBox(0, 200, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(200, 0, 20))], ) + second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + user_paths = [ + item + for item in routing_network.collect_route_carriers(doc) + if item.QetRouteCarrierKind == "UserPath" + ] - self.assertEqual(1, len(created)) - end_point = created[0].Points[-1] - self.assertEqual((0.0, 10.0, 20.0), (end_point.x, end_point.y, end_point.z)) + self.assertEqual(1, first["user_path_carriers"]) + self.assertEqual(1, second["user_path_carriers"]) + self.assertEqual(1, len(user_paths)) + self.assertEqual([(0.0, 0.0, 20.0), (200.0, 0.0, 20.0)], [(p.x, p.y, p.z) for p in user_paths[0].Points]) - def test_eplan_connection_route_enters_network_at_segment_projection(self): + def test_eplan_connection_route_can_use_generated_user_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(150, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(200, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual("segment_projection", result["network"]["entry_point_mode"]) - self.assertEqual("segment_projection", result["network"]["exit_point_mode"]) - self.assertNotIn(0.0, [point.x for point in result["points"][1:-1]]) - self.assertNotIn(200.0, [point.x for point in result["points"][1:-1]]) - self.assertLess(result["length_mm"], 150.0) + self.assertEqual("Routed", result["route_status"]) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) - def test_generate_routing_path_network_adds_wiring_cut_out_carrier(self): + def test_generate_routing_paths_does_not_duplicate_selected_wire_duct_carriers(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - cut_out = doc.addObject("Part::Feature", "WiringCutoutA") - cut_out.Label = "Wiring Cut-Out A" - cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) + duct = doc.addObject("Part::Feature", "UnlabeledLongDuct") + duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + ) - result = auto_routing_panel.AutoRoutingController().generate_routing_paths() - cut_out_carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" - ] + first = auto_routing_panel.AutoRoutingController().generate_routing_paths() + second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = routing_network.collect_route_carriers(doc) - self.assertEqual(1, result["wiring_cut_out_carriers"]) - self.assertEqual(1, len(cut_out_carriers)) - self.assertEqual("PassThrough", cut_out.QetRoutingObstacleMode) + self.assertEqual(1, first["selected_wire_duct_carriers"]) + self.assertEqual(0, second["selected_wire_duct_carriers"]) + self.assertEqual( + 1, + len([item for item in carriers if item.QetRouteCarrierKind == "WireDuct"]), + ) + self.assertEqual( + 2, + len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), + ) - def test_generate_routing_path_network_refreshes_wiring_cut_out_geometry(self): + def test_generate_routing_paths_refreshes_selected_wire_duct_geometry(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - cut_out = doc.addObject("Part::Feature", "WiringCutoutA") - cut_out.Label = "Wiring Cut-Out A" - cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) + duct = doc.addObject("Part::Feature", "UnlabeledLongDuct") + duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + ) - first = auto_routing_panel.AutoRoutingController().generate_routing_paths() - cut_out.Shape = FakeShape(FakeBoundBox(65, 75, -2, 2, 15, 25)) + auto_routing_panel.AutoRoutingController().generate_routing_paths() + duct.Shape = FakeShape(FakeBoundBox(0, 220, -10, 10, 0, 20)) second = auto_routing_panel.AutoRoutingController().generate_routing_paths() - cut_out_carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" - ] + carriers = routing_network.collect_route_carriers(doc) + main = [item for item in carriers if item.QetRouteCarrierKind == "WireDuct"][0] + open_end_x_values = sorted( + point.x + for item in carriers + if item.QetRouteCarrierKind == "WireDuctOpenEnd" + for point in item.Points + ) - self.assertEqual(1, first["wiring_cut_out_carriers"]) - self.assertEqual(0, second["wiring_cut_out_carriers"]) - self.assertEqual(1, len(cut_out_carriers)) - self.assertEqual([(70.0, -22.0, 20.0), (70.0, 22.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) + self.assertEqual(0, second["selected_wire_duct_carriers"]) + self.assertEqual([(20.0, 0.0, 10.0), (200.0, 0.0, 10.0)], [(p.x, p.y, p.z) for p in main.Points]) + self.assertEqual([20.0, 20.0, 200.0, 200.0], open_end_x_values) - def test_wiring_cut_out_source_bridge_extension_controls_generated_path_length(self): + def test_generate_routing_paths_removes_generated_wire_duct_carriers_after_source_deleted(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -5972,684 +6710,707 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - cut_out = doc.addObject("Part::Feature", "WiringCutoutA") - cut_out.Label = "过线孔A" - cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) - cut_out.QetWiringCutOutBridgeExtensionMm = 8.0 + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) auto_routing_panel.AutoRoutingController().generate_routing_paths() - cut_out_carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" + generated = [ + item + for item in routing_network.collect_route_carriers(doc) + if getattr(item, "QetRouteSourceName", "") == "WireDuctA" ] + doc.removeObject("WireDuctA") + auto_routing_panel.AutoRoutingController().generate_routing_paths() - self.assertEqual(1, len(cut_out_carriers)) - self.assertIn("QetWiringCutOutBridgeExtensionMm", cut_out.PropertiesList) - self.assertEqual(8.0, cut_out.QetWiringCutOutBridgeExtensionMm) - self.assertEqual([(50.0, -10.0, 20.0), (50.0, 10.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) + self.assertEqual(3, len(generated)) + self.assertEqual([], routing_network.collect_route_carriers(doc)) - def test_wiring_cut_out_bridges_nearby_ducts_on_both_sides_of_panel(self): + def test_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, -20, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 20, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, -20, 20), app.Vector(50, -20, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(50, 20, 20), app.Vector(100, 20, 20)], - project_uuid="project-1", - kind="WireDuct", + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "Mounting Plate A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=panel)], ) - cut_out = doc.addObject("Part::Feature", "WiringCutoutA") - cut_out.Label = "过线孔A" - cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) - auto_routing_panel.AutoRoutingController().generate_routing_paths() - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().generate_layout_space() - self.assertEqual("Routed", result["route_status"]) - self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"]) - self.assertEqual(0, result["collision_count"]) + self.assertGreater(result["support_surface_sources"], 0) + self.assertEqual("document", result["source_mode"]) - def test_check_routing_path_network_writes_diagnostic_for_unconnected_terminal(self): + def test_generate_routing_path_network_adds_terminal_access_to_route_network(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalFar", "terminal-far", app.Vector(5000, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - self.assertFalse(result["ok"]) - self.assertIn("unconnected_terminals", result["issue_codes"]) - self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) - self.assertEqual("project-1", diagnostic_group.Group[0].QetProjectUuid) - self.assertFalse(diagnostic_group.Group[0].QetDiagnosticOk) - self.assertIn("unconnected_terminals", diagnostic_group.Group[0].QetDiagnosticIssueCodes) - self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticIssueLabels) - self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticMessage) - self.assertIn("unconnected_terminals", payload["issue_codes"]) - self.assertEqual(1, len(payload["unconnected_terminals"])) - self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"]) - self.assertEqual(1000.0, payload["unconnected_terminals"][0]["terminal_access_max_distance_mm"]) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - self.assertIn("端子未接入", message) - self.assertIn("terminal-far", message) - self.assertIn("4900.0 mm", message) - self.assertIn("端子接入最大距离 1000.0 mm", message) - self.assertIn("补一段线槽/辅助路径", message) + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + result_again = auto_routing_panel.AutoRoutingController().generate_routing_paths() + access_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" + ] - def test_check_routing_path_network_warns_for_long_terminal_access(self): + self.assertEqual(1, result["wire_duct_carriers"]) + self.assertEqual(2, result["wire_duct_open_end_carriers"]) + self.assertEqual(2, result["terminal_access_carriers"]) + self.assertEqual(0, result_again["wire_duct_carriers"]) + self.assertEqual(0, result_again["wire_duct_open_end_carriers"]) + self.assertEqual(2, result_again["terminal_access_carriers"]) + self.assertEqual(2, len(access_carriers)) + self.assertGreater(result["network"]["segments"], 0) + + def test_generate_routing_path_network_connects_terminal_access_to_nearest_segment_point(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - device = doc.addObject("App::Part", "DevicePEN") - device.Label = "PEN" - device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) - terminal = _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) - device.addObject(terminal) + _terminal(doc, terminal_objects, "TerminalMid", "terminal-mid", app.Vector(50, 30, 0)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + + auto_routing_panel.AutoRoutingController().generate_routing_paths() + access_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" + ] + + self.assertEqual(1, len(access_carriers)) + end_point = access_carriers[0].Points[-1] + self.assertEqual((50.0, 0.0, 20.0), (end_point.x, end_point.y, end_point.z)) + + def test_terminal_access_prefers_larger_connected_network_over_nearer_isolated_stub(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(1000, 0, 20), app.Vector(1100, 0, 20)], + [app.Vector(0, 1, 20), app.Vector(5, 1, 20)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_terminal_access_carriers_from_document( + routing_network.create_route_carrier( doc, + [ + app.Vector(0, 10, 20), + app.Vector(40, 10, 20), + app.Vector(80, 10, 20), + app.Vector(120, 10, 20), + ], project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=1000.0, + kind="WireDuct", ) - result = auto_routing.check_eplan_routing_path_network( + created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", - options={"terminal_access_max_distance": 1000.0}, ) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - self.assertFalse(result["ok"]) - self.assertEqual(1, len(payload["long_terminal_accesses"])) - long_access = payload["long_terminal_accesses"][0] - self.assertEqual("terminal-long-access", long_access["terminal_uuid"]) - self.assertEqual(900.0, long_access["terminal_access_length_mm"]) - self.assertEqual("PEN", long_access["parent_device_label"]) - self.assertEqual("DevicePEN", long_access["parent_device_name"]) - self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, long_access["terminal_origin"]) - self.assertEqual("x", long_access["terminal_access_dominant_axis"]) - self.assertEqual(2, len(long_access["terminal_access_points"])) - self.assertEqual({"x": 100.0, "y": 0.0, "z": 20.0}, long_access["terminal_access_points"][0]) - self.assertEqual({"x": 1000.0, "y": 0.0, "z": 20.0}, long_access["terminal_access_points"][1]) - self.assertIn("端子接入过长", message) - self.assertIn("TerminalLongAccess", message) - self.assertIn("terminal-long-access", message) - self.assertIn("900.0 mm", message) + self.assertEqual(1, len(created)) + end_point = created[0].Points[-1] + self.assertEqual((0.0, 10.0, 20.0), (end_point.x, end_point.y, end_point.z)) - def test_check_routing_path_network_ignores_isolated_routing_range_only_components(self): + def test_terminal_access_prefers_richer_main_path_component_over_near_short_duct(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 10, 20), app.Vector(10, 10, 20)], project_uuid="project-1", kind="WireDuct", + label="近处短线槽孤岛", ) - routing_network.create_route_carrier( + for index in range(8): + routing_network.create_route_carrier( + doc, + [ + app.Vector(index * 30, 500, 20), + app.Vector((index + 1) * 30, 500, 20), + ], + project_uuid="project-1", + kind="WireDuct", + label="完整主线槽{0}".format(index + 1), + ) + + created = routing_network.create_terminal_access_carriers_from_document( doc, - [app.Vector(0, 0, 0), app.Vector(0, 40, 0)], project_uuid="project-1", - kind="TerminalAccess", + terminal_exit_length=20.0, + max_distance=1000.0, ) + + self.assertEqual(1, len(created)) + end_point = created[0].Points[-1] + self.assertEqual((0.0, 500.0, 20.0), (end_point.x, end_point.y, end_point.z)) + + def test_terminal_access_records_selected_target_metadata(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", - kind="RoutingRange", - label="孤立布线面", + kind="UserPath", + label="柜内主路径", ) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=200.0, + ) - self.assertNotIn("isolated_network_components", result["issue_codes"]) - self.assertEqual(0, len(result["diagnostic"]["isolated_components"])) + self.assertEqual(1, len(created)) + carrier = created[0] + self.assertEqual("UserPath", carrier.QetTerminalAccessTargetKind) + self.assertEqual("柜内主路径", carrier.QetTerminalAccessTargetLabel) + self.assertEqual(100.0, carrier.QetTerminalAccessTargetDistanceMm) + self.assertEqual(1, carrier.QetTerminalAccessTargetComponentPrimarySegments) - def test_check_routing_path_network_warns_for_isolated_primary_route_components(self): + def test_connection_entry_candidates_prefer_wire_duct_over_terminal_access(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 0), app.Vector(0, 40, 0)], + [app.Vector(0, 0, 20), app.Vector(0, 10, 20)], project_uuid="project-1", kind="TerminalAccess", ) routing_network.create_route_carrier( doc, - [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], project_uuid="project-1", - kind="UserPath", - label="孤立用户路径", + kind="WireDuct", ) + network = routing_network.build_route_graph(doc) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + ranked = routing_network.rank_connection_point_candidates( + network, + routing_network.connection_point_candidates(network, app.Vector(0, 0, 20), limit=0), + ) - self.assertIn("isolated_network_components", result["issue_codes"]) - self.assertEqual(2, len(result["diagnostic"]["isolated_components"])) + first_kind = getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "") + self.assertEqual("WireDuct", first_kind) - def test_check_routing_path_network_warns_for_wire_duct_without_terminal_access(self): + def test_auto_routing_consumes_terminal_access_carrier_as_endpoint_access_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", kind="WireDuct", - label="孤立线槽", + label="主线槽", ) - routing_network.create_route_carrier( + routing_network.create_terminal_access_carriers_from_document( doc, - [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", - kind="TerminalAccess", - label="端子接入", + terminal_exit_length=20.0, + max_distance=200.0, ) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 20.0, "terminal_access_max_distance": 200.0}, + ) - self.assertIn("wire_ducts_without_terminal_access", result["issue_codes"]) - self.assertEqual(1, len(result["diagnostic"]["wire_ducts_without_terminal_access"])) - suggestion = result["diagnostic"]["wire_ducts_without_terminal_access"][0]["bridge_suggestion"] - self.assertEqual("孤立线槽", suggestion["from_carrier"]["label"]) - self.assertEqual("端子接入", suggestion["to_carrier"]["label"]) - self.assertEqual(900.0, suggestion["distance_mm"]) - self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, suggestion["from_point"]) - self.assertEqual({"x": 1000.0, "y": 0.0, "z": 0.0}, suggestion["to_point"]) - compact_suggestion = payload["wire_ducts_without_terminal_access"][0]["bridge_suggestion"] - self.assertEqual("端子接入", compact_suggestion["to_carrier"]["label"]) - self.assertIn("线槽未接入端子主网络", message) - self.assertIn("建议桥接到 端子接入", message) - self.assertIn("900.0 mm", message) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertEqual(0.0, result["network"]["entry_distance"]) + self.assertEqual(0.0, result["network"]["exit_distance"]) + self.assertIn( + (0.0, 100.0, 20.0), + [(point.x, point.y, point.z) for point in result["points"]], + ) + self.assertIn( + (100.0, 100.0, 20.0), + [(point.x, point.y, point.z) for point in result["points"]], + ) - def test_zero_distance_user_path_endpoint_splits_wire_duct_segment(self): + def test_auto_routing_reports_consumed_terminal_access_carriers(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 100, 0)], + [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", kind="WireDuct", - label="斜向线槽", + label="主线槽", ) - routing_network.create_route_carrier( + created_access = routing_network.create_terminal_access_carriers_from_document( doc, - [app.Vector(50, 50, 0), app.Vector(50, 90, 0)], project_uuid="project-1", - kind="UserPath", - label="零距离桥接", + terminal_exit_length=20.0, + max_distance=200.0, ) - routing_network.create_route_carrier( + + result = auto_routing.route_eplan_connection_between_terminals( doc, - [app.Vector(50, 90, 0), app.Vector(50, 130, 0)], - project_uuid="project-1", - kind="TerminalAccess", - label="端子接入", + start, + end, + options={"terminal_exit_length": 20.0, "terminal_access_max_distance": 200.0}, ) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - - self.assertNotIn("wire_ducts_without_terminal_access", result["issue_codes"]) + self.assertEqual(2, len(created_access)) + self.assertTrue(result["network"]["start_terminal_access_consumed"]) + self.assertTrue(result["network"]["end_terminal_access_consumed"]) + self.assertIn( + result["network"]["start_terminal_access_carrier"], + [carrier.Name for carrier in created_access], + ) + self.assertIn( + result["network"]["end_terminal_access_carrier"], + [carrier.Name for carrier in created_access], + ) - def test_create_user_path_bridge_from_selection_connects_nearest_route_points(self): + def test_terminal_access_prefers_wire_duct_over_nearer_routing_range(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - duct = routing_network.create_route_carrier( + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], project_uuid="project-1", - kind="WireDuct", - label="线槽", + kind="RoutingRange", + label="近处布线面", ) - main_path = routing_network.create_route_carrier( + routing_network.create_route_carrier( doc, - [app.Vector(120, 20, 0), app.Vector(200, 20, 0)], + [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", - kind="RoutingRange", - label="主网络", + kind="WireDuct", + label="较远线槽", ) - created = routing_network.create_user_path_bridge_from_selection( + created = routing_network.create_terminal_access_carriers_from_document( doc, - [ - types.SimpleNamespace(Object=duct), - types.SimpleNamespace(Object=main_path), - ], project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=1000.0, ) self.assertEqual(1, len(created)) - self.assertEqual("UserPath", created[0].QetRouteCarrierKind) - self.assertEqual([(100.0, 0.0, 0.0), (120.0, 20.0, 0.0)], [ - (point.x, point.y, point.z) - for point in created[0].Points - ]) + end_point = created[0].Points[-1] + self.assertEqual((50.0, 100.0, 20.0), (end_point.x, end_point.y, end_point.z)) - def test_create_user_path_bridge_between_source_objects_uses_nearest_carrier_pair(self): + def test_terminal_access_prefers_distant_main_path_over_near_routing_range_within_limit(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") - fallback_source.Label = "门板布线面" - current_source = doc.addObject("Part::Feature", "MainDuctSource") - current_source.Label = "主线槽" - far_fallback = routing_network.create_route_carrier( - doc, - [app.Vector(-500, 0, 0), app.Vector(-400, 0, 0)], - project_uuid="project-1", - kind="RoutingRange", - label="远处布线面", - ) - near_fallback = routing_network.create_route_carrier( + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], project_uuid="project-1", kind="RoutingRange", - label="近处布线面", + label="近处安装板布线面", ) - main_path = routing_network.create_route_carrier( + routing_network.create_route_carrier( doc, - [app.Vector(130, 20, 0), app.Vector(200, 20, 0)], + [app.Vector(0, 7000, 20), app.Vector(100, 7000, 20)], project_uuid="project-1", kind="WireDuct", - label="主线槽路径", + label="远处主线槽", ) - for carrier in (far_fallback, near_fallback): - carrier.QetRouteSourceName = fallback_source.Name - carrier.QetRouteSourceLabel = fallback_source.Label - main_path.QetRouteSourceName = current_source.Name - main_path.QetRouteSourceLabel = current_source.Label - created = routing_network.create_user_path_bridge_between_objects( + created = routing_network.create_terminal_access_carriers_from_document( doc, - fallback_source, - current_source, project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=8000.0, ) self.assertEqual(1, len(created)) - self.assertEqual("UserPath", created[0].QetRouteCarrierKind) - self.assertEqual( - [(100.0, 0.0, 0.0), (130.0, 20.0, 0.0)], - [(point.x, point.y, point.z) for point in created[0].Points], + end_point = created[0].Points[-1] + self.assertEqual((50.0, 7000.0, 20.0), (end_point.x, end_point.y, end_point.z)) + + def test_terminal_access_uses_routing_range_when_no_main_path_target_exists(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面", ) - self.assertEqual("MainPathDetourBridge", created[0].QetRouteBridgeKind) - self.assertEqual("门板布线面 -> 主线槽", created[0].QetRouteBridgePairLabel) - self.assertEqual(fallback_source.Name, created[0].QetRouteBridgeLeftSourceName) - self.assertEqual(current_source.Name, created[0].QetRouteBridgeRightSourceName) - duplicated = routing_network.create_user_path_bridge_between_objects( + created = routing_network.create_terminal_access_carriers_from_document( doc, - fallback_source, - current_source, project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=100.0, ) - self.assertEqual([], duplicated) + self.assertEqual(1, len(created)) + end_point = created[0].Points[-1] + self.assertEqual((50.0, 5.0, 20.0), (end_point.x, end_point.y, end_point.z)) - def test_check_routing_path_network_warns_for_invalid_terminal_local_route_points(self): + def test_terminal_access_prefers_main_path_over_routing_range_in_same_component(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - terminal = _terminal(doc, terminal_objects, "TerminalInvalidLocalPath", "terminal-invalid-local-path", app.Vector(0, 0, 0)) - terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") - terminal.QetTerminalLocalRoutePointsJson = "{not-valid-json" + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="近处布线面", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", kind="WireDuct", + label="较远线槽", ) - routing_network.create_terminal_access_carriers_from_document( + routing_network.create_route_carrier( + doc, + [app.Vector(50, 5, 20), app.Vector(50, 100, 20)], + project_uuid="project-1", + kind="UserPath", + label="线槽接入桥", + ) + network = routing_network.build_route_graph(doc) + ranked = routing_network.rank_connection_point_candidates( + network, + routing_network.connection_point_candidates(network, app.Vector(50, 0, 20), limit=0), + ) + + created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=1000.0, ) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + self.assertEqual(1, len(created)) + self.assertEqual("UserPath", getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "")) + end_point = created[0].Points[-1] + self.assertEqual((50.0, 5.0, 20.0), (end_point.x, end_point.y, end_point.z)) - self.assertFalse(result["ok"]) - self.assertEqual(1, len(payload["invalid_terminal_local_routes"])) - self.assertEqual( - "terminal-invalid-local-path", - payload["invalid_terminal_local_routes"][0]["terminal_uuid"], + def test_diverse_connection_entry_candidates_keep_multiple_components(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + near = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + label="近组件", ) - self.assertEqual( - "QetTerminalLocalRoutePointsJson", - payload["invalid_terminal_local_routes"][0]["property_name"], + far = routing_network.create_route_carrier( + doc, + [app.Vector(0, 100, 0), app.Vector(100, 100, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="远组件", + ) + network = routing_network.build_route_graph(doc) + near_key = routing_network._point_key(app.Vector(0, 0, 0)) + far_key = routing_network._point_key(app.Vector(0, 100, 0)) + candidates = [ + { + "key": near_key, + "projected_key": routing_network._point_key(app.Vector(index, 0, 0)), + "point": app.Vector(index, 0, 0), + "distance": index, + "carrier": near, + } + for index in range(1, 6) + ] + candidates.append( + { + "key": far_key, + "projected_key": far_key, + "point": app.Vector(0, 100, 0), + "distance": 100.0, + "carrier": far, + } ) - self.assertIn("端子局部路径无效", message) - self.assertIn("terminal-invalid-local-path", message) - def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): + selected = routing_network.select_diverse_connection_point_candidates(network, candidates, limit=3) + + self.assertEqual(3, len(selected)) + self.assertIn(far, [candidate.get("carrier") for candidate in selected]) + + def test_terminal_access_prefers_wire_duct_over_nearer_routing_range(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - terminal = _terminal(doc, terminal_objects, "TerminalLocalEndOnDuct", "terminal-local-end-on-duct", app.Vector(0, 0, 0)) - terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") - terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [1000, 0, 0]]) + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + [app.Vector(0, 1, 20), app.Vector(120, 1, 20)], project_uuid="project-1", - kind="WireDuct", + kind="RoutingRange", ) - created = routing_network.create_terminal_access_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(0, 10, 20), app.Vector(120, 10, 20)], project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=100.0, + kind="WireDuct", ) - result = auto_routing.check_eplan_routing_path_network( + created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", - options={"terminal_access_max_distance": 100.0}, - ) - - self.assertEqual([], created) - self.assertEqual([], result["diagnostic"]["unconnected_terminals"]) - self.assertNotIn( - "unconnected_terminals", - [issue.get("code") for issue in result["diagnostic"]["issues"]], ) - def test_format_routing_path_network_report_tolerates_malformed_samples(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - diagnostic = { - "issues": [{"code": "external_issue", "count": 1}], - "unconnected_terminals": ["bad-terminal-sample"], - "possible_breaks": ["bad-break-sample"], - "isolated_components": ["bad-component-sample"], - } - - message = auto_routing.format_routing_path_network_report(diagnostic) - - self.assertIn("布线路径网络检查发现", message) - self.assertIn("首个问题:external_issue", message) + self.assertEqual(1, len(created)) + end_point = created[0].Points[-1] + self.assertEqual((0.0, 10.0, 20.0), (end_point.x, end_point.y, end_point.z)) - def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): + def test_eplan_connection_route_enters_network_at_segment_projection(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(150, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="线槽A", + [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertIn("线槽端点疑似断开", message) - self.assertIn("线槽A", message) - self.assertIn("(0.0, 0.0, 20.0)", message) - self.assertIn("补齐相邻线槽", message) + self.assertEqual("segment_projection", result["network"]["entry_point_mode"]) + self.assertEqual("segment_projection", result["network"]["exit_point_mode"]) + self.assertNotIn(0.0, [point.x for point in result["points"][1:-1]]) + self.assertNotIn(200.0, [point.x for point in result["points"][1:-1]]) + self.assertLess(result["length_mm"], 150.0) - def test_check_routing_path_network_warns_when_network_is_empty(self): + def test_generate_routing_path_network_adds_wiring_cut_out_carrier(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") + cut_out = doc.addObject("Part::Feature", "WiringCutoutA") + cut_out.Label = "Wiring Cut-Out A" + cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + cut_out_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" + ] - self.assertFalse(result["ok"]) - self.assertEqual("empty_routing_path_network", payload["issues"][0]["code"]) - self.assertEqual(0, payload["summary"]["segments"]) - self.assertIn("布线路径网络为空", message) + self.assertEqual(1, result["wiring_cut_out_carriers"]) + self.assertEqual(1, len(cut_out_carriers)) + self.assertEqual("PassThrough", cut_out.QetRoutingObstacleMode) - def test_check_routing_path_network_warns_for_invalid_route_carrier_geometry(self): + def test_generate_routing_path_network_refreshes_wiring_cut_out_geometry(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - carrier = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="坏用户路径", - project_uuid="project-1", - kind="UserPath", - ) - carrier.Points = [app.Vector(0, 0, 20)] + cut_out = doc.addObject("Part::Feature", "WiringCutoutA") + cut_out.Label = "Wiring Cut-Out A" + cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + first = auto_routing_panel.AutoRoutingController().generate_routing_paths() + cut_out.Shape = FakeShape(FakeBoundBox(65, 75, -2, 2, 15, 25)) + second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + cut_out_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" + ] - self.assertFalse(result["ok"]) - self.assertEqual(1, len(payload["invalid_route_carriers"])) - self.assertEqual("UserPath", payload["invalid_route_carriers"][0]["carrier"]["kind"]) - self.assertEqual(1, payload["invalid_route_carriers"][0]["point_count"]) - self.assertIn("路径对象几何无效", message) - self.assertIn("坏用户路径", message) + self.assertEqual(1, first["wiring_cut_out_carriers"]) + self.assertEqual(0, second["wiring_cut_out_carriers"]) + self.assertEqual(1, len(cut_out_carriers)) + self.assertEqual([(70.0, -22.0, 20.0), (70.0, 22.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) - def test_check_routing_path_network_warns_when_only_routing_range_is_available(self): + def test_wiring_cut_out_source_bridge_extension_controls_generated_path_length(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="RoutingRange", - ) - - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - - self.assertFalse(result["ok"]) - self.assertEqual(1, payload["routing_range_only_network"]["routing_range_carriers"]) - self.assertEqual( - 0, - payload["routing_range_only_network"]["primary_route_carriers"], - ) - self.assertIn("routing_range_only_network", [issue.get("code") for issue in payload["issues"]]) - self.assertIn("仅使用布线面兜底", message) - - def test_format_routing_path_network_report_includes_bridged_segment_count(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - diagnostic = { - "summary": { - "carriers": 5, - "segments": 6, - "nodes": 5, - "bridged_segments": 1, - }, - "issues": [], - "ok": True, - } - - message = auto_routing.format_routing_path_network_report(diagnostic) - - self.assertIn("桥接 1 段相邻/投影主路径", message) - - def test_routing_path_network_diagnostic_message_tolerates_malformed_bridge_count(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - diagnostic = { - "summary": { - "carriers": 1, - "segments": 1, - "nodes": 2, - "bridged_segments": "not-a-number", - }, - "issues": [], - } + cut_out = doc.addObject("Part::Feature", "WiringCutoutA") + cut_out.Label = "过线孔A" + cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) + cut_out.QetWiringCutOutBridgeExtensionMm = 8.0 - message = routing_network._routing_path_network_diagnostic_message(diagnostic) + auto_routing_panel.AutoRoutingController().generate_routing_paths() + cut_out_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" + ] - self.assertIn("布线路径网络检查通过", message) + self.assertEqual(1, len(cut_out_carriers)) + self.assertIn("QetWiringCutOutBridgeExtensionMm", cut_out.PropertiesList) + self.assertEqual(8.0, cut_out.QetWiringCutOutBridgeExtensionMm) + self.assertEqual([(50.0, -10.0, 20.0), (50.0, 10.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) - def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self): + def test_wiring_cut_out_bridges_nearby_ducts_on_both_sides_of_panel(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - for index, points in enumerate( - ( - [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], - [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], - [app.Vector(100, 0, 20), app.Vector(100, 100, 20)], - [app.Vector(100, 100, 20), app.Vector(0, 100, 20)], - [app.Vector(0, 100, 20), app.Vector(0, 0, 20)], - ), - start=1, - ): - routing_network.create_route_carrier( - doc, - points, - label="线槽{0}".format(index), - project_uuid="project-1", - kind="WireDuct", - ) - - result = auto_routing.check_eplan_routing_path_network( + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, -20, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 20, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, -20, 20), app.Vector(50, -20, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( doc, + [app.Vector(50, 20, 20), app.Vector(100, 20, 20)], project_uuid="project-1", - options={"adjoining_duct_tolerance": 15.0}, + kind="WireDuct", ) + cut_out = doc.addObject("Part::Feature", "WiringCutoutA") + cut_out.Label = "过线孔A" + cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) - self.assertTrue(result["ok"]) - self.assertEqual(1, result["diagnostic"]["summary"]["bridged_segments"]) - self.assertEqual([], result["diagnostic"]["possible_breaks"]) - - def test_generate_routing_path_network_skips_far_terminal_access_to_protect_view_bbox(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") - app = sys.modules["FreeCAD"] - doc = FakeDocument() - app.ActiveDocument = doc - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - duct = doc.addObject("Part::Feature", "WireDuctFar") - duct.Label = "Wire Duct Far" - duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) - - result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + auto_routing_panel.AutoRoutingController().generate_routing_paths() + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual(1, result["wire_duct_carriers"]) - self.assertEqual(2, result["wire_duct_open_end_carriers"]) - self.assertEqual(0, result["terminal_access_carriers"]) + self.assertEqual("Routed", result["route_status"]) + self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"]) + self.assertEqual(0, result["collision_count"]) - def test_auto_routing_controller_exposes_terminal_access_max_distance(self): + def test_check_routing_path_network_writes_diagnostic_for_unconnected_terminal(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - duct = doc.addObject("Part::Feature", "WireDuctFar") - duct.Label = "Wire Duct Far" - duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) + _terminal(doc, terminal_objects, "TerminalFar", "terminal-far", app.Vector(5000, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) - controller = auto_routing_panel.AutoRoutingController() - controller.set_terminal_access_max_distance(6000.0) - result = controller.generate_routing_paths() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - self.assertEqual(1, result["terminal_access_carriers"]) - self.assertEqual(6000.0, controller.routing_options()["terminal_access_max_distance"]) + self.assertFalse(result["ok"]) + self.assertIn("unconnected_terminals", result["issue_codes"]) + self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) + self.assertEqual("project-1", diagnostic_group.Group[0].QetProjectUuid) + self.assertFalse(diagnostic_group.Group[0].QetDiagnosticOk) + self.assertIn("unconnected_terminals", diagnostic_group.Group[0].QetDiagnosticIssueCodes) + self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticIssueLabels) + self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticMessage) + self.assertIn("unconnected_terminals", payload["issue_codes"]) + self.assertEqual(1, len(payload["unconnected_terminals"])) + self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"]) + self.assertEqual(1000.0, payload["unconnected_terminals"][0]["terminal_access_max_distance_mm"]) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + self.assertIn("端子未接入", message) + self.assertIn("terminal-far", message) + self.assertIn("4900.0 mm", message) + self.assertIn("端子接入最大距离 1000.0 mm", message) + self.assertIn("补一段线槽/辅助路径", message) - def test_auto_routing_controller_exposes_terminal_access_warning_distance(self): + def test_check_routing_path_network_warns_for_long_terminal_access(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) + device = doc.addObject("App::Part", "DevicePEN") + device.Label = "PEN" + device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) + terminal = _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) + device.addObject(terminal) routing_network.create_route_carrier( doc, - [app.Vector(900, 0, 20), app.Vector(1000, 0, 20)], + [app.Vector(1000, 0, 20), app.Vector(1100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) @@ -6659,1461 +7420,1525 @@ class AutoRoutingTest(unittest.TestCase): terminal_exit_length=20.0, max_distance=1000.0, ) - controller = auto_routing_panel.AutoRoutingController() - controller.set_terminal_access_max_distance(1000.0) - controller.set_terminal_access_warning_distance(950.0) - result = controller.check_routing_path_network() - - self.assertNotIn("long_terminal_accesses", result["issue_codes"]) - self.assertEqual(950.0, controller.routing_options()["terminal_access_warning_distance"]) - - def test_auto_routing_controller_exposes_terminal_exit_length(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") - app = sys.modules["FreeCAD"] - doc = FakeDocument() - app.ActiveDocument = doc - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "Wire Duct A" - duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - - controller = auto_routing_panel.AutoRoutingController() - controller.set_terminal_exit_length(40.0) - controller.generate_routing_paths() - access_carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" - ] - - self.assertEqual(1, len(access_carriers)) - self.assertEqual( - (50.0, 0.0, 40.0), - tuple(getattr(access_carriers[0].Points[0], axis) for axis in ("x", "y", "z")), + result = auto_routing.check_eplan_routing_path_network( + doc, + project_uuid="project-1", + options={"terminal_access_max_distance": 1000.0}, ) - self.assertEqual(40.0, controller.routing_options()["terminal_exit_length"]) - - def test_auto_routing_controller_readiness_writes_preflight_diagnostic(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") - app = sys.modules["FreeCAD"] - doc = FakeDocument() - app.ActiveDocument = doc - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-missing", - } - ], - } - - report = auto_routing_panel.AutoRoutingController().check_routing_readiness() diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - self.assertIn("missing_endpoints", report["issue_codes"]) - self.assertIsNotNone(diagnostic_group) - self.assertEqual(1, len(diagnostic_group.Group)) - self.assertEqual("RoutingPreflight", diagnostic_group.Group[0].QetDiagnosticKind) + self.assertFalse(result["ok"]) + self.assertEqual(1, len(payload["long_terminal_accesses"])) + long_access = payload["long_terminal_accesses"][0] + self.assertEqual("terminal-long-access", long_access["terminal_uuid"]) + self.assertEqual(900.0, long_access["terminal_access_length_mm"]) + self.assertEqual("PEN", long_access["parent_device_label"]) + self.assertEqual("DevicePEN", long_access["parent_device_name"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, long_access["terminal_origin"]) + self.assertEqual("x", long_access["terminal_access_dominant_axis"]) + self.assertEqual(2, len(long_access["terminal_access_points"])) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 20.0}, long_access["terminal_access_points"][0]) + self.assertEqual({"x": 1000.0, "y": 0.0, "z": 20.0}, long_access["terminal_access_points"][1]) + self.assertIn("端子接入过长", message) + self.assertIn("TerminalLongAccess", message) + self.assertIn("terminal-long-access", message) + self.assertIn("900.0 mm", message) - def test_route_eplan_connections_prepares_layout_space_like_eplan_route(self): + def test_check_routing_path_network_ignores_isolated_routing_range_only_components(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "Wire Duct A" - duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } - ], - } + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(0, 40, 0)], + project_uuid="project-1", + kind="TerminalAccess", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="孤立布线面", + ) - report = auto_routing_panel.AutoRoutingController().route_eplan_connections() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - self.assertEqual(1, report["routed"]) - self.assertEqual("eplan-route-v1", report["routing_method"]) - self.assertTrue(report["routing_path_network_updated"]) - self.assertEqual(1, report["prepared_layout"]["wire_duct_carriers"]) - self.assertEqual(1, report["routing_path_network"]["wire_duct_carriers"]) - self.assertEqual(2, report["prepared_layout"]["terminal_access_carriers"]) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - self.assertIsNotNone(diagnostic_group) - self.assertEqual(1, len(diagnostic_group.Group)) - diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - self.assertEqual(1, diagnostic_payload["prepared_layout"]["wire_duct_carriers"]) - self.assertEqual(2, diagnostic_payload["prepared_layout"]["terminal_access_carriers"]) + self.assertNotIn("isolated_network_components", result["issue_codes"]) + self.assertEqual(0, len(result["diagnostic"]["isolated_components"])) - def test_auto_routing_controller_passes_adjoining_duct_tolerance_to_batch_route(self): + def test_check_routing_path_network_warns_for_isolated_primary_route_components(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, - [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(0, 40, 0)], project_uuid="project-1", - kind="WireDuct", + kind="TerminalAccess", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + project_uuid="project-1", + kind="UserPath", + label="孤立用户路径", ) - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } - ], - } - report = auto_routing_panel.AutoRoutingController( - options={"adjoining_duct_tolerance": 15.0} - ).route_eplan_connections() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - self.assertEqual(1, report["routed"]) - self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) + self.assertIn("isolated_network_components", result["issue_codes"]) + self.assertEqual(2, len(result["diagnostic"]["isolated_components"])) - def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(self): + def test_check_routing_path_network_warns_for_wire_duct_without_terminal_access(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", + label="孤立线槽", ) routing_network.create_route_carrier( doc, - [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", - kind="WireDuct", + kind="TerminalAccess", + label="端子接入", ) - summary = auto_routing_panel.AutoRoutingController( - options={"adjoining_duct_tolerance": 15.0} - ).summary() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - self.assertIn("桥接:1", summary) + self.assertIn("wire_ducts_without_terminal_access", result["issue_codes"]) + self.assertEqual(1, len(result["diagnostic"]["wire_ducts_without_terminal_access"])) + suggestion = result["diagnostic"]["wire_ducts_without_terminal_access"][0]["bridge_suggestion"] + self.assertEqual("孤立线槽", suggestion["from_carrier"]["label"]) + self.assertEqual("端子接入", suggestion["to_carrier"]["label"]) + self.assertEqual(900.0, suggestion["distance_mm"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, suggestion["from_point"]) + self.assertEqual({"x": 1000.0, "y": 0.0, "z": 0.0}, suggestion["to_point"]) + compact_suggestion = payload["wire_ducts_without_terminal_access"][0]["bridge_suggestion"] + self.assertEqual("端子接入", compact_suggestion["to_carrier"]["label"]) + self.assertIn("线槽未接入端子主网络", message) + self.assertIn("建议桥接到 端子接入", message) + self.assertIn("900.0 mm", message) - def test_auto_routing_controller_summary_includes_runtime_version(self): + def test_check_routing_path_network_compact_payload_includes_terminal_access_fallback_targets(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalFallbackAccess", "terminal-fallback", app.Vector(0, 0, 0)) + fallback = routing_network.create_route_carrier( + doc, + [app.Vector(20, 0, 20), app.Vector(20, 80, 20)], + label="安装板兜底路径", + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + ) - summary = auto_routing_panel.AutoRoutingController().summary() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - self.assertIn("版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), summary) + self.assertIn("terminal_access_fallback_targets", result["issue_codes"]) + self.assertEqual(1, len(payload["terminal_access_fallback_targets"])) + sample = payload["terminal_access_fallback_targets"][0] + self.assertEqual("TerminalFallbackAccess", sample["terminal_name"]) + self.assertEqual("terminal-fallback", sample["terminal_uuid"]) + access_carrier = doc.getObject(sample["access_carrier_name"]) + self.assertIsNotNone(access_carrier) + self.assertEqual("TerminalAccess", access_carrier.QetRouteCarrierKind) + self.assertEqual(fallback.Name, sample["target_name"]) + self.assertEqual("RoutingRange", sample["target_kind"]) + self.assertEqual(20.0, sample["access_length_mm"]) + self.assertEqual( + [{"x": 0.0, "y": 0.0, "z": 20.0}, {"x": 20.0, "y": 0.0, "z": 20.0}], + sample["access_points"], + ) - def test_auto_routing_controller_summary_includes_cabinet_boundary_count(self): + def test_check_routing_path_network_compact_payload_includes_terminal_access_endpoint_avoidance(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc - terminal_objects.ensure_root_group(doc, "project-1") - boundary = doc.addObject("Part::Feature", "CabinetInteriorSpace") - boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) - boundary.QetRoutingBoundaryKind = "CabinetInterior" + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceAccessBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalWithLocalExit", "terminal-local-exit", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [20, 0, 0]]) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "EndpointDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 10)) + device.addObject(body) + routing_network.create_route_carrier( + doc, + [app.Vector(-20, 0, 0), app.Vector(-20, 80, 0)], + label="左侧主路径", + project_uuid="project-1", + kind="UserPath", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + ) - summary = auto_routing_panel.AutoRoutingController().summary() - - self.assertIn("柜内边界:1", summary) + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - def test_auto_routing_controller_summary_includes_wire_style_database_path(self): + self.assertIn("terminal_access_endpoint_device_avoidance", result["issue_codes"]) + self.assertEqual(1, len(payload["terminal_access_endpoint_device_avoidance"])) + sample = payload["terminal_access_endpoint_device_avoidance"][0] + self.assertEqual("TerminalWithLocalExit", sample["terminal_name"]) + self.assertEqual("terminal-local-exit", sample["terminal_uuid"]) + self.assertEqual("QETDeviceAccessBox", sample["parent_device_name"]) + access_carrier = doc.getObject(sample["access_carrier_name"]) + self.assertIsNotNone(access_carrier) + self.assertEqual("TerminalAccess", access_carrier.QetRouteCarrierKind) + self.assertEqual("UserPath", sample["target_kind"]) + self.assertGreater(sample["access_length_mm"], 0.0) + self.assertGreaterEqual(len(sample["access_points"]), 2) + + def test_check_routing_path_network_compact_payload_includes_terminal_exit_diagnostics(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc - app._qet_exchange_summary = { - "wire_style_database_path": "D:/project/project-local.sqlite", - } - terminal_objects.ensure_root_group(doc, "project-1") + root = terminal_objects.ensure_root_group(doc, "project-1") - summary = auto_routing_panel.AutoRoutingController().summary() + corrected_device = doc.addObject("App::DocumentObjectGroup", "QETDeviceCorrectedExit") + root.addObject(corrected_device) + corrected = _terminal(doc, terminal_objects, "TerminalCorrectedExit", "terminal-corrected", app.Vector(0, 0, 0)) + corrected_device.addObject(corrected) + corrected_body = doc.addObject("Part::Feature", "CorrectedDeepBody") + corrected_body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 500)) + corrected_device.addObject(corrected_body) + + capped_device = doc.addObject("App::DocumentObjectGroup", "QETDeviceCappedExit") + root.addObject(capped_device) + capped = _terminal(doc, terminal_objects, "TerminalCappedExit", "terminal-capped", app.Vector(100, 0, 0)) + capped.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + capped.QetTerminalExitDirectionJson = json.dumps({"x": 0, "y": 0, "z": 1}) + capped_device.addObject(capped) + capped_body = doc.addObject("Part::Feature", "CappedDeepBody") + capped_body.Shape = FakeShape(FakeBoundBox(90, 110, -10, 10, -10, 500)) + capped_device.addObject(capped_body) - self.assertIn("导线样式库:D:/project/project-local.sqlite", summary) + routing_network.create_route_carrier( + doc, + [app.Vector(20, 0, 0), app.Vector(120, 0, 0)], + label="侧向主路径", + project_uuid="project-1", + kind="UserPath", + ) - def test_auto_routing_controller_summary_reads_wire_style_database_path_from_payload(self): + result = auto_routing.check_eplan_routing_path_network( + doc, + project_uuid="project-1", + options={"terminal_exit_length": 20.0, "terminal_exit_max_length": 30.0}, + ) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + + self.assertIn("terminal_exit_direction_corrected", result["issue_codes"]) + self.assertIn("terminal_exit_length_capped", result["issue_codes"]) + self.assertEqual(1, len(payload["corrected_terminal_exits"])) + corrected_sample = payload["corrected_terminal_exits"][0] + self.assertEqual("TerminalCorrectedExit", corrected_sample["name"]) + self.assertEqual("terminal-corrected", corrected_sample["terminal_uuid"]) + self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, corrected_sample["exit_direction"]) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, corrected_sample["original_exit_direction"]) + self.assertEqual(1, len(payload["capped_terminal_exits"])) + capped_sample = payload["capped_terminal_exits"][0] + self.assertEqual("TerminalCappedExit", capped_sample["name"]) + self.assertEqual("terminal-capped", capped_sample["terminal_uuid"]) + self.assertEqual(30.0, capped_sample["max_exit_length_mm"]) + self.assertEqual(30.0, capped_sample["actual_exit_length_mm"]) + self.assertTrue(capped_sample["exit_length_capped"]) + + def test_compact_routing_path_network_diagnostic_keeps_terminal_access_quality_samples(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") - app = sys.modules["FreeCAD"] - doc = FakeDocument() - app.ActiveDocument = doc - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wire_style_database_path": "D:/project/payload-style.sqlite", - "wires": [], + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + diagnostic = { + "ok": False, + "issues": [ + {"severity": "warning", "code": "terminal_access_fallback_targets", "count": 1}, + {"severity": "info", "code": "terminal_access_endpoint_device_avoidance", "count": 1}, + ], + "terminal_access_fallback_targets": [ + { + "terminal_name": "TerminalFallbackAccess", + "terminal_label": "P1", + "terminal_uuid": "terminal-fallback", + "access_carrier_name": "TerminalAccessFallback001", + "access_carrier_label": "P1 接入段", + "instance_id": "instance-fallback", + "parent_device_name": "DeviceQF1", + "parent_device_label": "QF1", + "target_kind": "RoutingRange", + "target_name": "RoutingRange001", + "target_label": "安装板兜底路径", + "target_rule": "fallback_only", + "target_distance_mm": 35.0, + } + ], + "terminal_access_endpoint_device_avoidance": [ + { + "terminal_name": "TerminalWithLocalExit", + "terminal_label": "A1", + "terminal_uuid": "terminal-local-exit", + "access_carrier_name": "TerminalAccessAvoid001", + "access_carrier_label": "A1 接入段", + "instance_id": "instance-local-exit", + "parent_device_name": "DeviceKA1", + "parent_device_label": "KA1", + "target_kind": "UserPath", + "target_name": "UserPath001", + "target_label": "左侧主路径", + "target_rule": "main_path_nearest", + "target_distance_mm": 40.0, + } + ], } - terminal_objects.ensure_root_group(doc, "project-1") - - summary = auto_routing_panel.AutoRoutingController().summary() - self.assertIn("导线样式库:D:/project/payload-style.sqlite", summary) + payload = auto_routing._compact_routing_path_network_diagnostic(diagnostic) + + self.assertEqual(1, len(payload["terminal_access_fallback_targets"])) + fallback_sample = payload["terminal_access_fallback_targets"][0] + self.assertEqual("TerminalFallbackAccess", fallback_sample["terminal_name"]) + self.assertEqual("terminal-fallback", fallback_sample["terminal_uuid"]) + self.assertEqual("DeviceQF1", fallback_sample["parent_device_name"]) + self.assertEqual("TerminalAccessFallback001", fallback_sample["access_carrier_name"]) + self.assertEqual("P1 接入段", fallback_sample["access_carrier_label"]) + self.assertEqual("RoutingRange001", fallback_sample["target_name"]) + self.assertEqual("fallback_only", fallback_sample["target_rule"]) + self.assertEqual(1, len(payload["terminal_access_endpoint_device_avoidance"])) + avoidance_sample = payload["terminal_access_endpoint_device_avoidance"][0] + self.assertEqual("TerminalWithLocalExit", avoidance_sample["terminal_name"]) + self.assertEqual("terminal-local-exit", avoidance_sample["terminal_uuid"]) + self.assertEqual("DeviceKA1", avoidance_sample["parent_device_name"]) + self.assertEqual("TerminalAccessAvoid001", avoidance_sample["access_carrier_name"]) + self.assertEqual("A1 接入段", avoidance_sample["access_carrier_label"]) + self.assertEqual("UserPath001", avoidance_sample["target_name"]) - def test_auto_routing_controller_summary_prefers_current_payload_style_database_path(self): + def test_zero_distance_user_path_endpoint_splits_wire_duct_segment(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc - app._qet_exchange_summary = { - "project_uuid": "project-old", - "wire_style_database_path": "D:/old/project-local.sqlite", - } - app._qet_exchange_payload = { - "project_uuid": "project-current", - "wire_style_database_path": "D:/current/project-local.sqlite", - "wires": [], - } - terminal_objects.ensure_root_group(doc, "project-current") + terminal_objects.ensure_root_group(doc, "project-1") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 100, 0)], + project_uuid="project-1", + kind="WireDuct", + label="斜向线槽", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(50, 50, 0), app.Vector(50, 90, 0)], + project_uuid="project-1", + kind="UserPath", + label="零距离桥接", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(50, 90, 0), app.Vector(50, 130, 0)], + project_uuid="project-1", + kind="TerminalAccess", + label="端子接入", + ) - summary = auto_routing_panel.AutoRoutingController().summary() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - self.assertIn("导线样式库:D:/current/project-local.sqlite", summary) - self.assertNotIn("D:/old/project-local.sqlite", summary) + self.assertNotIn("wire_ducts_without_terminal_access", result["issue_codes"]) - def test_auto_routing_controller_summary_includes_route_constraint_counts(self): + def test_create_user_path_bridge_from_selection_connects_nearest_route_points(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - required = routing_network.create_route_carrier( + duct = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", - kind="UserPath", + kind="WireDuct", + label="线槽", ) - required.QetRouteConstraintMode = "Required" - forbidden = routing_network.create_route_carrier( + main_path = routing_network.create_route_carrier( doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + [app.Vector(120, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", - kind="UserPath", + kind="RoutingRange", + label="主网络", ) - forbidden.QetRouteConstraintMode = "Forbidden" - summary = auto_routing_panel.AutoRoutingController().summary() + created = routing_network.create_user_path_bridge_from_selection( + doc, + [ + types.SimpleNamespace(Object=duct), + types.SimpleNamespace(Object=main_path), + ], + project_uuid="project-1", + ) - self.assertIn("路径约束:必经 1,禁经 1", summary) + self.assertEqual(1, len(created)) + self.assertEqual("UserPath", created[0].QetRouteCarrierKind) + self.assertEqual([(100.0, 0.0, 0.0), (120.0, 20.0, 0.0)], [ + (point.x, point.y, point.z) + for point in created[0].Points + ]) - def test_auto_routing_controller_summary_includes_source_route_constraint_counts(self): + def test_create_user_path_bridge_between_source_objects_uses_nearest_carrier_pair(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "FutureUserRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") + fallback_source.Label = "门板布线面" + current_source = doc.addObject("Part::Feature", "MainDuctSource") + current_source.Label = "主线槽" + far_fallback = routing_network.create_route_carrier( + doc, + [app.Vector(-500, 0, 0), app.Vector(-400, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="远处布线面", ) - route_path.QetRouteConstraintMode = "Required" + near_fallback = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="近处布线面", + ) + main_path = routing_network.create_route_carrier( + doc, + [app.Vector(130, 20, 0), app.Vector(200, 20, 0)], + project_uuid="project-1", + kind="WireDuct", + label="主线槽路径", + ) + for carrier in (far_fallback, near_fallback): + carrier.QetRouteSourceName = fallback_source.Name + carrier.QetRouteSourceLabel = fallback_source.Label + main_path.QetRouteSourceName = current_source.Name + main_path.QetRouteSourceLabel = current_source.Label - summary = auto_routing_panel.AutoRoutingController().summary() + created = routing_network.create_user_path_bridge_between_objects( + doc, + fallback_source, + current_source, + project_uuid="project-1", + ) - self.assertIn("源路径约束:必经 1,禁经 0", summary) + self.assertEqual(1, len(created)) + self.assertEqual("UserPath", created[0].QetRouteCarrierKind) + self.assertEqual( + [(100.0, 0.0, 0.0), (130.0, 20.0, 0.0)], + [(point.x, point.y, point.z) for point in created[0].Points], + ) + self.assertEqual("MainPathDetourBridge", created[0].QetRouteBridgeKind) + self.assertEqual("门板布线面 -> 主线槽", created[0].QetRouteBridgePairLabel) + self.assertEqual(fallback_source.Name, created[0].QetRouteBridgeLeftSourceName) + self.assertEqual(current_source.Name, created[0].QetRouteBridgeRightSourceName) - def test_auto_routing_controller_summary_counts_wire_duct_source_route_constraints(self): + duplicated = routing_network.create_user_path_bridge_between_objects( + doc, + fallback_source, + current_source, + project_uuid="project-1", + ) + + self.assertEqual([], duplicated) + + def test_check_routing_path_network_warns_for_invalid_terminal_exit_direction(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - wire_duct_source = doc.addObject("Part::Feature", "WireDuctBody") - wire_duct_source.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) - wire_duct_source.Shape.Solids = [object()] - wire_duct_source.QetRoutingSourceKind = "WireDuct" - wire_duct_source.QetRouteConstraintMode = "Forbidden" + terminal = _terminal(doc, terminal_objects, "TerminalInvalidDirection", "terminal-invalid-direction", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + terminal.QetTerminalExitDirectionJson = "{not-valid-json" + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=1000.0, + ) - summary = auto_routing_panel.AutoRoutingController().summary() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - self.assertIn("源路径约束:必经 0,禁经 1", summary) + self.assertFalse(result["ok"]) + self.assertIn("invalid_terminal_exit_directions", payload["issue_codes"]) + self.assertEqual(1, len(payload["invalid_terminal_exit_directions"])) + self.assertEqual( + "terminal-invalid-direction", + payload["invalid_terminal_exit_directions"][0]["terminal_uuid"], + ) + self.assertEqual( + "QetTerminalExitDirectionJson", + payload["invalid_terminal_exit_directions"][0]["property_name"], + ) + self.assertIn("端子出线方向无效", message) + self.assertIn("terminal-invalid-direction", message) - def test_auto_routing_controller_exposes_lane_spacing(self): + def test_check_routing_path_network_warns_for_invalid_terminal_local_route_points(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) + terminal = _terminal(doc, terminal_objects, "TerminalInvalidLocalPath", "terminal-invalid-local-path", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = "{not-valid-json" routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, - ], - } + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=1000.0, + ) - controller = auto_routing_panel.AutoRoutingController() - controller.set_lane_spacing(14.0) - report = controller.route_eplan_connections() + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - self.assertEqual(14.0, controller.routing_options()["lane_spacing"]) - self.assertEqual(14.0, report["routes"][1]["lane"]["spacing_mm"]) - self.assertEqual(14.0, report["routes"][1]["lane"]["offset_mm"]) + self.assertFalse(result["ok"]) + self.assertEqual(1, len(payload["invalid_terminal_local_routes"])) + self.assertEqual( + "terminal-invalid-local-path", + payload["invalid_terminal_local_routes"][0]["terminal_uuid"], + ) + self.assertEqual( + "QetTerminalLocalRoutePointsJson", + payload["invalid_terminal_local_routes"][0]["property_name"], + ) + self.assertEqual( + "invalid_json", + payload["invalid_terminal_local_routes"][0]["reason"], + ) + self.assertIn("端子局部路径无效", message) + self.assertIn("terminal-invalid-local-path", message) - def test_auto_routing_controller_exposes_lane_axis(self): + def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + terminal = _terminal(doc, terminal_objects, "TerminalLocalEndOnDuct", "terminal-local-end-on-duct", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [1000, 0, 0]]) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, - ], + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=100.0, + ) + + result = auto_routing.check_eplan_routing_path_network( + doc, + project_uuid="project-1", + options={"terminal_access_max_distance": 100.0}, + ) + + self.assertEqual([], created) + self.assertEqual([], result["diagnostic"]["unconnected_terminals"]) + self.assertNotIn( + "unconnected_terminals", + [issue.get("code") for issue in result["diagnostic"]["issues"]], + ) + + def test_format_routing_path_network_report_tolerates_malformed_samples(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + diagnostic = { + "issues": [{"code": "external_issue", "count": 1}], + "unconnected_terminals": ["bad-terminal-sample"], + "possible_breaks": ["bad-break-sample"], + "isolated_components": ["bad-component-sample"], } - controller = auto_routing_panel.AutoRoutingController() - controller.set_lane_spacing(8.0) - controller.set_lane_axis("z") - report = controller.route_eplan_connections() + message = auto_routing.format_routing_path_network_report(diagnostic) - self.assertEqual("z", controller.routing_options()["lane_axis"]) - self.assertEqual("z", report["routes"][1]["lane"]["axis"]) - self.assertEqual(8.0, report["routes"][1]["lane"]["offset_mm"]) + self.assertIn("布线路径网络检查发现", message) + self.assertIn("首个问题:external_issue", message) - def test_auto_routing_controller_exposes_lane_max_offset(self): + def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="线槽A", project_uuid="project-1", kind="WireDuct", ) - controller = auto_routing_panel.AutoRoutingController() - controller.set_lane_spacing(10.0) - controller.set_lane_axis("y") - controller.set_lane_max_offset(18.0) - result = _auto_routing.route_eplan_connection_between_terminals( + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + + self.assertIn("线槽端点疑似断开", message) + self.assertIn("线槽A", message) + self.assertIn("(0.0, 0.0, 20.0)", message) + self.assertIn("补齐相邻线槽", message) + + def test_check_routing_path_network_warns_when_network_is_empty(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + + self.assertFalse(result["ok"]) + self.assertEqual("empty_routing_path_network", payload["issues"][0]["code"]) + self.assertEqual(0, payload["summary"]["segments"]) + self.assertIn("布线路径网络为空", message) + + def test_check_routing_path_network_warns_for_invalid_route_carrier_geometry(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + carrier = routing_network.create_route_carrier( doc, - start, - end, - route_index=21, - options=controller.routing_options(), + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="坏用户路径", + project_uuid="project-1", + kind="UserPath", ) + carrier.Points = [app.Vector(0, 0, 20)] - self.assertEqual(18.0, controller.routing_options()["lane_max_offset"]) - self.assertEqual(18.0, result["lane"]["max_offset_mm"]) - self.assertEqual(18.0, result["lane"]["offset_mm"]) + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) - def test_auto_routing_controller_exposes_obstacle_clearance(self): + self.assertFalse(result["ok"]) + self.assertEqual(1, len(payload["invalid_route_carriers"])) + self.assertEqual("UserPath", payload["invalid_route_carriers"][0]["carrier"]["kind"]) + self.assertEqual(1, payload["invalid_route_carriers"][0]["point_count"]) + self.assertIn("路径对象几何无效", message) + self.assertIn("坏用户路径", message) + + def test_check_routing_path_network_warns_when_only_routing_range_is_available(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - kind="WireDuct", + kind="RoutingRange", ) - obstacle = doc.addObject("Part::Feature", "NearObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 3, 6, 15, 25)) - controller = auto_routing_panel.AutoRoutingController() - controller.set_obstacle_clearance(5.0) - result = _auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options=controller.routing_options(), + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + + self.assertFalse(result["ok"]) + self.assertEqual(1, payload["routing_range_only_network"]["routing_range_carriers"]) + self.assertEqual( + 0, + payload["routing_range_only_network"]["primary_route_carriers"], ) + self.assertIn("routing_range_only_network", [issue.get("code") for issue in payload["issues"]]) + self.assertIn("仅使用布线面兜底", message) - self.assertEqual(5.0, controller.routing_options()["obstacle_clearance"]) - self.assertEqual("CollisionWarning", result["route_status"]) - self.assertEqual("ClearanceWarning", result["collisions"][0]["collision_kind"]) - self.assertEqual(["QET Route Carrier"], result["collisions"][0]["route_source_labels"]) - diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) - self.assertEqual(["QET Route Carrier"], diagnostics["collisions"][0]["route_source_labels"]) + def test_format_routing_path_network_report_includes_bridged_segment_count(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + diagnostic = { + "summary": { + "carriers": 5, + "segments": 6, + "nodes": 5, + "bridged_segments": 1, + }, + "issues": [], + "ok": True, + } - def test_auto_routing_controller_exposes_preflight_routeability_sample_limit(self): + message = auto_routing.format_routing_path_network_report(diagnostic) + + self.assertIn("桥接 1 段相邻/投影主路径", message) + + def test_routing_path_network_diagnostic_message_tolerates_malformed_bridge_count(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + diagnostic = { + "summary": { + "carriers": 1, + "segments": 1, + "nodes": 2, + "bridged_segments": "not-a-number", + }, + "issues": [], + } - controller = auto_routing_panel.AutoRoutingController() - controller.set_preflight_routeability_sample_limit(75) + message = routing_network._routing_path_network_diagnostic_message(diagnostic) - self.assertEqual(75, controller.routing_options()["preflight_routeability_sample_limit"]) + self.assertIn("布线路径网络检查通过", message) - def test_auto_routing_controller_exposes_segment_reuse_penalty(self): + def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - auto_routing_panel = importlib.import_module("AutoRoutingPanel") + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="Direct Duct", - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], - label="Left Bridge", - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="Alternate Duct", - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( + for index, points in enumerate( + ( + [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(100, 0, 20), app.Vector(100, 100, 20)], + [app.Vector(100, 100, 20), app.Vector(0, 100, 20)], + [app.Vector(0, 100, 20), app.Vector(0, 0, 20)], + ), + start=1, + ): + routing_network.create_route_carrier( + doc, + points, + label="线槽{0}".format(index), + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.check_eplan_routing_path_network( doc, - [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], - label="Right Bridge", project_uuid="project-1", - kind="WireDuct", + options={"adjoining_duct_tolerance": 15.0}, ) - app._qet_exchange_payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, - ], - } - - controller = auto_routing_panel.AutoRoutingController() - controller.set_segment_reuse_penalty(0.0) - report = controller.route_eplan_connections() - second_labels = [ - segment["carrier"]["label"] - for segment in report["routes"][1]["route_track"]["segments"] - ] - self.assertEqual(0.0, controller.routing_options()["segment_reuse_penalty"]) - self.assertIn("Direct Duct", second_labels) - self.assertNotIn("Alternate Duct", second_labels) + self.assertTrue(result["ok"]) + self.assertEqual(1, result["diagnostic"]["summary"]["bridged_segments"]) + self.assertEqual([], result["diagnostic"]["possible_breaks"]) - def test_auto_routing_panel_command_button_style_keeps_text_visible(self): + def test_generate_routing_path_network_skips_far_terminal_access_to_protect_view_bbox(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") - - class FakeButton: - def __init__(self): - self.text = "" - self.tooltip = "" - self.minimum_height = 0 - self.stylesheet = "" - - def setText(self, text): - self.text = text - - def setToolTip(self, tooltip): - self.tooltip = tooltip - - def setMinimumHeight(self, height): - self.minimum_height = height - - def setStyleSheet(self, stylesheet): - self.stylesheet = stylesheet - - button = FakeButton() - - auto_routing_panel._style_command_button(button, "生成布线连接", "按导线任务布线") - - self.assertEqual("生成布线连接", button.text) - self.assertEqual("按导线任务布线", button.tooltip) - self.assertGreaterEqual(button.minimum_height, 28) - self.assertIn("color", button.stylesheet) - - def test_eplan_connection_route_rejects_far_network_entry_to_avoid_huge_render_bbox(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(5000, 0, 20), app.Vector(5100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuctFar") + duct.Label = "Wire Duct Far" + duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) - with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() - def test_route_eplan_connection_between_terminals_fails_without_network(self): + self.assertEqual(1, result["wire_duct_carriers"]) + self.assertEqual(2, result["wire_duct_open_end_carriers"]) + self.assertEqual(0, result["terminal_access_carriers"]) + + def test_auto_routing_controller_exposes_terminal_access_max_distance(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 30, 0)) + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuctFar") + duct.Label = "Wire Duct Far" + duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) - with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual(0, len(wiring_objects.iter_routed_wire_objects(doc))) + controller = auto_routing_panel.AutoRoutingController() + controller.set_terminal_access_max_distance(6000.0) + result = controller.generate_routing_paths() - def test_surface_carrier_grid_uses_actual_rotated_face_plane(self): + self.assertEqual(1, result["terminal_access_carriers"]) + self.assertEqual(6000.0, controller.routing_options()["terminal_access_max_distance"]) + + def test_auto_routing_controller_exposes_terminal_access_warning_distance(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - normal = app.Vector(0, 1, 1) - vertices = [ - app.Vector(0, 0, 0), - app.Vector(100, 0, 0), - app.Vector(0, 50, -50), - app.Vector(100, 50, -50), - ] - face = FakeFace( - FakeBoundBox(0, 100, 0, 50, -50, 0), - normal, - vertices=vertices, - center=app.Vector(50, 25, -25), - ) - - created = routing_network.create_surface_carriers_from_selection( + _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) + routing_network.create_route_carrier( doc, - [FakeSelectionItem([face])], + [app.Vector(900, 0, 20), app.Vector(1000, 0, 20)], project_uuid="project-1", - spacing=50.0, - offset=10.0, - margin=0.0, - ) - - self.assertGreater(len(created), 0) - first_point = created[0].Points[0] - for carrier in created: - for point in carrier.Points: - # The rotated face is y + z = 0; after a 10 mm normal offset, - # all generated points must stay on one parallel plane. - self.assertAlmostEqual(first_point.y + first_point.z, point.y + point.z, places=6) - - def test_route_path_creation_ignores_whole_solid_object_edges(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - solid = doc.addObject("Part::Feature", "CabinetSolid") - solid.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 100, 0, 10), - edges=[FakeEdge(app.Vector(0, 0, 0), app.Vector(100, 0, 0))], - faces=[object()], + kind="WireDuct", ) - - created = routing_network.create_carriers_from_selection( + routing_network.create_terminal_access_carriers_from_document( doc, - [FakeSelectionItem(obj=solid)], project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=1000.0, ) + controller = auto_routing_panel.AutoRoutingController() + controller.set_terminal_access_max_distance(1000.0) + controller.set_terminal_access_warning_distance(950.0) - self.assertEqual([], created) + result = controller.check_routing_path_network() - def test_route_path_creation_splits_disconnected_shape_wires(self): + self.assertNotIn("long_terminal_accesses", result["issue_codes"]) + self.assertEqual(950.0, controller.routing_options()["terminal_access_warning_distance"]) + + def test_auto_routing_controller_exposes_terminal_exit_length(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") - route_path.Shape = FakeShape( - FakeBoundBox(0, 120, 0, 80, 20, 20), - wires=[ - FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), - FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), - ], - ) + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - created = routing_network.create_carriers_from_selection( - doc, - [FakeSelectionItem(obj=route_path)], - project_uuid="project-1", - ) + controller = auto_routing_panel.AutoRoutingController() + controller.set_terminal_exit_length(40.0) + controller.generate_routing_paths() + access_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" + ] - self.assertEqual(2, len(created)) + self.assertEqual(1, len(access_carriers)) self.assertEqual( - [ - [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0)], - [(80.0, 80.0, 20.0), (120.0, 80.0, 20.0)], - ], - [[(point.x, point.y, point.z) for point in carrier.Points] for carrier in created], + (50.0, 0.0, 40.0), + tuple(getattr(access_carriers[0].Points[0], axis) for axis in ("x", "y", "z")), ) + self.assertEqual(40.0, controller.routing_options()["terminal_exit_length"]) - def test_route_path_creation_projects_line_to_selected_face(self): + def test_auto_routing_controller_readiness_writes_preflight_diagnostic(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - face = FakeFace( - FakeBoundBox(0, 100, 0, 100, 0, 0), - app.Vector(0, 0, 1), - ) - draft_line = doc.addObject("Part::Feature", "DraftLine") - draft_line.Shape = FakeShape( - FakeBoundBox(10, 90, 10, 90, 25, 35), - edges=[FakeEdge(app.Vector(10, 10, 25), app.Vector(90, 90, 35))], - ) - - created = routing_network.create_carriers_from_selection( - doc, - [ - FakeSelectionItem([face]), - FakeSelectionItem(obj=draft_line), + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + } ], - project_uuid="project-1", - ) - - self.assertEqual(1, len(created)) - self.assertEqual([2.0, 2.0], [point.z for point in created[0].Points]) - - def test_wire_duct_entity_generates_centerline_and_marks_source_pass_through(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - duct = doc.addObject("Part::Feature", "WireDuct") - duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) + } - created = routing_network.create_wire_duct_carriers_from_selection( - doc, - [FakeSelectionItem(obj=duct)], - project_uuid="project-1", - margin=20.0, - ) + report = auto_routing_panel.AutoRoutingController().check_routing_readiness() + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - self.assertEqual(3, len(created)) - carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0] - open_ends = [item for item in created if item.QetRouteCarrierKind == "WireDuctOpenEnd"] - self.assertEqual("WireDuct", carrier.QetRouteCarrierKind) - self.assertEqual(2, len(open_ends)) - self.assertEqual("PassThrough", duct.QetRoutingObstacleMode) - self.assertEqual([(20.0, 0.0, 15.0), (100.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points]) + self.assertIn("missing_endpoints", report["issue_codes"]) + self.assertIsNotNone(diagnostic_group) + self.assertEqual(1, len(diagnostic_group.Group)) + self.assertEqual("RoutingPreflight", diagnostic_group.Group[0].QetDiagnosticKind) - def test_wire_duct_source_end_margin_controls_generated_centerline_length(self): + def test_route_eplan_connections_prepares_layout_space_like_eplan_route(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "线槽A" - duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - duct.QetWireDuctEndMarginMm = 5.0 - - created = routing_network.create_wire_duct_carriers_from_document( - doc, - project_uuid="project-1", - ) - - carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0] - self.assertIn("QetWireDuctEndMarginMm", duct.PropertiesList) - self.assertEqual(5.0, duct.QetWireDuctEndMarginMm) - self.assertEqual([(5.0, 0.0, 15.0), (115.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points]) - - def test_wire_duct_source_capacity_is_copied_to_generated_carriers(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - duct = doc.addObject("Part::Feature", "WireDuct") - duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - duct.QetRouteCarrierCapacity = 4 + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } - created = routing_network.create_wire_duct_carriers_from_selection( - doc, - [FakeSelectionItem(obj=duct)], - project_uuid="project-1", - margin=20.0, - ) + report = auto_routing_panel.AutoRoutingController().route_eplan_connections() - self.assertIn("QetRouteCarrierCapacity", duct.PropertiesList) - self.assertTrue(all(item.QetRouteCarrierCapacity == 4 for item in created)) + self.assertEqual(1, report["routed"]) + self.assertEqual("eplan-route-v1", report["routing_method"]) + self.assertTrue(report["routing_path_network_updated"]) + self.assertEqual(1, report["prepared_layout"]["wire_duct_carriers"]) + self.assertEqual(1, report["routing_path_network"]["wire_duct_carriers"]) + self.assertEqual(2, report["prepared_layout"]["terminal_access_carriers"]) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + self.assertIsNotNone(diagnostic_group) + self.assertEqual(1, len(diagnostic_group.Group)) + diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + self.assertEqual(1, diagnostic_payload["prepared_layout"]["wire_duct_carriers"]) + self.assertEqual(2, diagnostic_payload["prepared_layout"]["terminal_access_carriers"]) - def test_auto_detect_wire_ducts_ignores_cabinet_models(self): + def test_auto_routing_controller_passes_adjoining_duct_tolerance_to_batch_route(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "线槽A" - duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - cabinet = doc.addObject("Part::Feature", "Cabinet") - cabinet.Label = "3D机柜" - cabinet.Shape = FakeShape(FakeBoundBox(0, 300, 0, 80, 0, 400)) - - created = routing_network.create_wire_duct_carriers_from_document( - doc, - project_uuid="project-1", - ) - created_again = routing_network.create_wire_duct_carriers_from_document( + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) + routing_network.create_route_carrier( doc, + [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], project_uuid="project-1", + kind="WireDuct", ) - - self.assertEqual(3, len(created)) - self.assertEqual(0, len(created_again)) - self.assertEqual(1, len([item for item in created if item.QetRouteCarrierKind == "WireDuct"])) - self.assertEqual(2, len([item for item in created if item.QetRouteCarrierKind == "WireDuctOpenEnd"])) - self.assertEqual("PassThrough", duct.QetRoutingObstacleMode) - self.assertFalse(hasattr(cabinet, "QetRoutingObstacleMode")) - - def test_wire_duct_source_is_not_reported_as_collision(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) - duct = doc.addObject("Part::Feature", "WireDuct") - duct.Shape = FakeShape(FakeBoundBox(-10, 130, -10, 10, 15, 25)) - routing_network.create_wire_duct_carriers_from_selection( + routing_network.create_route_carrier( doc, - [FakeSelectionItem(obj=duct)], + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - margin=0.0, + kind="WireDuct", ) + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + report = auto_routing_panel.AutoRoutingController( + options={"adjoining_duct_tolerance": 15.0} + ).route_eplan_connections() - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) + self.assertEqual(1, report["routed"]) + self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) - def test_eplan_connection_route_uses_alternate_carrier_to_avoid_obstacle(self): + def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, - [ - app.Vector(0, 0, 20), - app.Vector(0, 50, 20), - app.Vector(100, 50, 20), - app.Vector(100, 0, 20), - ], + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - obstacle = doc.addObject("Part::Feature", "CabinetObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 15, 25)) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + summary = auto_routing_panel.AutoRoutingController( + options={"adjoining_duct_tolerance": 15.0} + ).summary() - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) - self.assertTrue(result["network"]["obstacle_aware"]) - self.assertGreaterEqual(result["network"]["blocked_segments"], 1) - self.assertIn(50.0, [point.y for point in result["points"]]) + self.assertIn("桥接:1", summary) - def test_eplan_connection_route_prefers_entry_candidate_without_access_collision(self): + def test_auto_routing_controller_summary_includes_runtime_version(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(20, 0, 0), app.Vector(100, 0, 0)], - label="Near Duct", - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 30, 0), app.Vector(100, 30, 0)], - label="Clear Duct", - project_uuid="project-1", - kind="WireDuct", - ) - obstacle = doc.addObject("Part::Feature", "AccessObstacle") - obstacle.Label = "Access Obstacle" - obstacle.Shape = FakeShape(FakeBoundBox(10, 15, -5, 5, -5, 5)) - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + summary = auto_routing_panel.AutoRoutingController().summary() - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("Clear Duct", labels) - self.assertNotIn("Near Duct", labels) - self.assertEqual(0, result["collision_count"]) + self.assertIn("版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), summary) - def test_eplan_connection_route_keeps_clear_access_candidates_beyond_distance_limit(self): + def test_auto_routing_controller_summary_includes_cabinet_boundary_count(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - for index in range(9): - routing_network.create_route_carrier( - doc, - [app.Vector(20, index, 0), app.Vector(100, index, 0)], - label="Near Blocked Duct {0}".format(index + 1), - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 30, 0), app.Vector(100, 30, 0)], - label="Clear Duct", - project_uuid="project-1", - kind="WireDuct", - ) - obstacle = doc.addObject("Part::Feature", "AccessObstacle") - obstacle.Label = "Access Obstacle" - obstacle.Shape = FakeShape(FakeBoundBox(10, 15, -5, 20, -5, 5)) + boundary = doc.addObject("Part::Feature", "CabinetInteriorSpace") + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + boundary.QetRoutingBoundaryKind = "CabinetInterior" - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + summary = auto_routing_panel.AutoRoutingController().summary() - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("Clear Duct", labels) - self.assertTrue(all(not label.startswith("Near Blocked Duct") for label in labels)) - self.assertEqual(0, result["network"]["route_candidate_obstacle_hits"]) + self.assertIn("柜内边界:1", summary) - def test_eplan_connection_route_prefers_carrier_inside_cabinet_boundary(self): + def test_auto_routing_controller_summary_includes_wire_style_database_path(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc + app._qet_exchange_summary = { + "wire_style_database_path": "D:/project/project-local.sqlite", + } terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 49, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 51, 0), app.Vector(100, 51, 0)], - label="Outside Cabinet Path", - project_uuid="project-1", - kind="UserPath", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, -40, 0), app.Vector(100, -40, 0)], - label="Inside Cabinet Path", - project_uuid="project-1", - kind="UserPath", - ) - boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") - boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) - boundary.QetRoutingBoundaryKind = "CabinetInterior" - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + summary = auto_routing_panel.AutoRoutingController().summary() - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("Inside Cabinet Path", labels) - self.assertNotIn("Outside Cabinet Path", labels) - self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) + self.assertIn("导线样式库:D:/project/project-local.sqlite", summary) - def test_eplan_connection_route_prefers_inside_detour_over_shorter_outside_shortcut(self): + def test_auto_routing_controller_summary_reads_wire_style_database_path_from_payload(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wire_style_database_path": "D:/project/payload-style.sqlite", + "wires": [], + } terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("导线样式库:D:/project/payload-style.sqlite", summary) + + def test_auto_routing_controller_summary_prefers_current_payload_style_database_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + app._qet_exchange_summary = { + "project_uuid": "project-old", + "wire_style_database_path": "D:/old/project-local.sqlite", + } + app._qet_exchange_payload = { + "project_uuid": "project-current", + "wire_style_database_path": "D:/current/project-local.sqlite", + "wires": [], + } + terminal_objects.ensure_root_group(doc, "project-current") + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("导线样式库:D:/current/project-local.sqlite", summary) + self.assertNotIn("D:/old/project-local.sqlite", summary) + + def test_auto_routing_controller_summary_includes_route_constraint_counts(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + required = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 51, 0), app.Vector(100, 0, 0)], - label="Outside Shortcut", + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="UserPath", ) - routing_network.create_route_carrier( + required.QetRouteConstraintMode = "Required" + forbidden = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(0, -40, 0), app.Vector(100, -40, 0), app.Vector(100, 0, 0)], - label="Inside Cabinet Detour", + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], project_uuid="project-1", kind="UserPath", ) - boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") - boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) - boundary.QetRoutingBoundaryKind = "CabinetInterior" + forbidden.QetRouteConstraintMode = "Forbidden" - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + summary = auto_routing_panel.AutoRoutingController().summary() - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("Inside Cabinet Detour", labels) - self.assertNotIn("Outside Shortcut", labels) - self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) + self.assertIn("路径约束:必经 1,禁经 1", summary) - def test_eplan_connection_wire_records_boundary_warning_when_route_leaves_cabinet(self): + def test_auto_routing_controller_summary_includes_source_route_constraint_counts(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 49, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 60, 0), app.Vector(100, 60, 0)], - label="Only Outside Cabinet Path", - project_uuid="project-1", - kind="UserPath", + route_path = doc.addObject("Sketcher::SketchObject", "FutureUserRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) - boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") - boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) - boundary.QetRoutingBoundaryKind = "CabinetInterior" + route_path.QetRouteConstraintMode = "Required" - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + summary = auto_routing_panel.AutoRoutingController().summary() - self.assertGreater(result["network"]["route_candidate_boundary_violations"], 0) - self.assertTrue(result["wire"].QetRouteBoundaryAware) - self.assertEqual("BoundaryWarning", result["wire"].QetRouteBoundaryStatus) - self.assertEqual( - str(result["network"]["route_candidate_boundary_violations"]), - result["wire"].QetRouteBoundaryViolationCount, - ) + self.assertIn("源路径约束:必经 1,禁经 0", summary) - def test_eplan_connection_wire_records_long_network_access_warning(self): + def test_auto_routing_controller_summary_counts_wire_duct_source_route_constraints(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 125, 0), app.Vector(100, 125, 0)], - label="Far Cabinet Main Path", - project_uuid="project-1", - kind="UserPath", - ) + wire_duct_source = doc.addObject("Part::Feature", "WireDuctBody") + wire_duct_source.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) + wire_duct_source.Shape.Solids = [object()] + wire_duct_source.QetRoutingSourceKind = "WireDuct" + wire_duct_source.QetRouteConstraintMode = "Forbidden" - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={ - "terminal_exit_length": 0.0, - "lane_spacing": 0.0, - "terminal_access_warning_distance": 50.0, - }, - ) + summary = auto_routing_panel.AutoRoutingController().summary() - wire = result["wire"] - self.assertEqual("125.000", wire.QetRouteEntryDistanceMm) - self.assertEqual("125.000", wire.QetRouteExitDistanceMm) - self.assertEqual("node", wire.QetRouteEntryPointMode) - self.assertEqual("node", wire.QetRouteExitPointMode) - self.assertEqual("1", wire.QetRouteEntryCandidateRank) - self.assertEqual("1", wire.QetRouteExitCandidateRank) - self.assertEqual("50.000", wire.QetRouteAccessWarningDistanceMm) - self.assertEqual("LongAccessWarning", wire.QetRouteAccessStatus) - self.assertEqual("entry,exit", wire.QetRouteAccessWarningSides) - payload = json.loads(wire.QetRouteDiagnosticsJson) - self.assertEqual("LongAccessWarning", payload["access"]["access_status"]) - self.assertEqual(["entry", "exit"], payload["access"]["warning_sides"]) + self.assertIn("源路径约束:必经 0,禁经 1", summary) - def test_eplan_connection_route_keeps_inside_boundary_candidates_beyond_distance_limit(self): + def test_auto_routing_controller_exposes_lane_spacing(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 49, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) - for index in range(9): - y = 51 + index - routing_network.create_route_carrier( - doc, - [app.Vector(0, y, 0), app.Vector(100, y, 0)], - label="Outside Candidate {0}".format(index + 1), - project_uuid="project-1", - kind="UserPath", - ) + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, -40, 0), app.Vector(100, -40, 0)], - label="Inside Cabinet Path", + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") - boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) - boundary.QetRoutingBoundaryKind = "CabinetInterior" + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + controller = auto_routing_panel.AutoRoutingController() + controller.set_lane_spacing(14.0) + report = controller.route_eplan_connections() - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("Inside Cabinet Path", labels) - self.assertTrue(all(not label.startswith("Outside Candidate") for label in labels)) - self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) + self.assertEqual(14.0, controller.routing_options()["lane_spacing"]) + self.assertEqual(14.0, report["routes"][1]["lane"]["spacing_mm"]) + self.assertEqual(14.0, report["routes"][1]["lane"]["offset_mm"]) - def test_eplan_connection_route_tolerates_missing_route_constraint_collector(self): + def test_auto_routing_controller_exposes_lane_axis(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="主路径", + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - collector = routing_network.collect_route_constraint_options - delattr(routing_network, "collect_route_constraint_options") - try: - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) - finally: - routing_network.collect_route_constraint_options = collector + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } - self.assertEqual("Routed", result["route_status"]) - self.assertEqual({}, result["network"].get("route_constraints", {})) + controller = auto_routing_panel.AutoRoutingController() + controller.set_lane_spacing(8.0) + controller.set_lane_axis("z") + report = controller.route_eplan_connections() - def test_eplan_connection_route_avoids_forbidden_carrier_label(self): + self.assertEqual("z", controller.routing_options()["lane_axis"]) + self.assertEqual("z", report["routes"][1]["lane"]["axis"]) + self.assertEqual(8.0, report["routes"][1]["lane"]["offset_mm"]) + + def test_auto_routing_controller_exposes_lane_max_offset(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="禁止路径", - project_uuid="project-1", - kind="UserPath", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="允许路径", project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - result = auto_routing.route_eplan_connection_between_terminals( + controller = auto_routing_panel.AutoRoutingController() + controller.set_lane_spacing(10.0) + controller.set_lane_axis("y") + controller.set_lane_max_offset(18.0) + result = _auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={ - "terminal_exit_length": 0.0, - "lane_spacing": 0.0, - "forbidden_route_carrier_labels": ["禁止路径"], - }, + route_index=21, + options=controller.routing_options(), ) - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("允许路径", labels) - self.assertNotIn("禁止路径", labels) + self.assertEqual(18.0, controller.routing_options()["lane_max_offset"]) + self.assertEqual(18.0, result["lane"]["max_offset_mm"]) + self.assertEqual(18.0, result["lane"]["offset_mm"]) - def test_eplan_connection_route_avoids_carrier_marked_forbidden(self): + def test_auto_routing_controller_exposes_obstacle_clearance(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - forbidden = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="近路径", - project_uuid="project-1", - kind="UserPath", - ) - forbidden.QetRouteConstraintMode = "Forbidden" routing_network.create_route_carrier( doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="远路径", + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) + obstacle = doc.addObject("Part::Feature", "NearObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 3, 6, 15, 25)) - result = auto_routing.route_eplan_connection_between_terminals( + controller = auto_routing_panel.AutoRoutingController() + controller.set_obstacle_clearance(5.0) + result = _auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + options=controller.routing_options(), ) - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("远路径", labels) - self.assertNotIn("近路径", labels) - self.assertIn( - forbidden.Name, - result["network"]["route_constraints"]["forbidden"]["names"], - ) + self.assertEqual(5.0, controller.routing_options()["obstacle_clearance"]) + self.assertEqual("CollisionWarning", result["route_status"]) + self.assertEqual("ClearanceWarning", result["collisions"][0]["collision_kind"]) + self.assertEqual(["QET Route Carrier"], result["collisions"][0]["route_source_labels"]) + diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) + self.assertEqual(["QET Route Carrier"], diagnostics["collisions"][0]["route_source_labels"]) - def test_eplan_connection_route_accepts_chinese_constraint_mode_aliases(self): + def test_auto_routing_controller_exposes_preflight_routeability_sample_limit(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + + controller = auto_routing_panel.AutoRoutingController() + controller.set_preflight_routeability_sample_limit(75) + + self.assertEqual(75, controller.routing_options()["preflight_routeability_sample_limit"]) + + def test_auto_routing_controller_exposes_auto_bridge_options(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + + controller = auto_routing_panel.AutoRoutingController() + controller.set_auto_create_diagnostic_bridges(True) + controller.set_auto_create_main_path_detour_bridges(True) + controller.set_auto_create_terminal_access_fallback_bridges(True) + + self.assertTrue(controller.routing_options()["auto_create_diagnostic_bridges"]) + self.assertTrue(controller.routing_options()["auto_create_main_path_detour_bridges"]) + self.assertTrue(controller.routing_options()["auto_create_terminal_access_fallback_bridges"]) + + def test_auto_routing_controller_exposes_segment_reuse_penalty(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - forbidden = routing_network.create_route_carrier( + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="近路径", + label="Direct Duct", project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - forbidden.QetRouteConstraintMode = "禁止经过" routing_network.create_route_carrier( doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="远路径", + [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], + label="Left Bridge", project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - - result = auto_routing.route_eplan_connection_between_terminals( + routing_network.create_route_carrier( doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) - - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("远路径", labels) - self.assertNotIn("近路径", labels) - self.assertIn( - forbidden.Name, - result["network"]["route_constraints"]["forbidden"]["names"], + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Alternate Duct", + project_uuid="project-1", + kind="WireDuct", ) - - def test_eplan_connection_route_uses_carrier_marked_required(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="近路径", - project_uuid="project-1", - kind="UserPath", - ) - required = routing_network.create_route_carrier( - doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="远路径", + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - required.QetRouteConstraintMode = "Required" + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + controller = auto_routing_panel.AutoRoutingController() + controller.set_segment_reuse_penalty(0.0) + report = controller.route_eplan_connections() - labels = [ + second_labels = [ segment["carrier"]["label"] - for segment in result["route_track"]["segments"] + for segment in report["routes"][1]["route_track"]["segments"] ] - self.assertIn("远路径", labels) - self.assertNotIn("近路径", labels) - self.assertIn( - required.Name, - result["network"]["route_constraints"]["required"]["names"], - ) + self.assertEqual(0.0, controller.routing_options()["segment_reuse_penalty"]) + self.assertIn("Direct Duct", second_labels) + self.assertNotIn("Alternate Duct", second_labels) - def test_source_required_constraint_from_multi_wire_sketch_accepts_one_generated_path(self): + def test_auto_routing_panel_command_button_style_keeps_text_visible(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - route_path = doc.addObject("Sketcher::SketchObject", "YellowMainRouteSketch") - route_path.Label = "黄色主路径" - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - wires=[ - FakeWire([app.Vector(0, 0, 20), app.Vector(100, 0, 20)]), - FakeWire([app.Vector(0, 80, 20), app.Vector(100, 80, 20)]), - ], - ) - selection = [FakeSelectionItem(obj=route_path)] - routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - carriers = routing_network.create_user_path_carriers_from_selection( - doc, - selection, - project_uuid="project-1", - ) + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + class FakeButton: + def __init__(self): + self.text = "" + self.tooltip = "" + self.minimum_height = 0 + self.stylesheet = "" - route_carrier_names = [ - segment["carrier"]["name"] - for segment in result["route_track"]["segments"] - if not segment.get("is_bridge") - ] - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertIn(carriers[0].Name, route_carrier_names) - self.assertNotIn(carriers[1].Name, route_carrier_names) - self.assertEqual( - ["黄色主路径"], - result["network"]["route_constraints"]["required"]["source_labels"], - ) + def setText(self, text): + self.text = text - def test_eplan_connection_route_requires_carrier_label(self): + def setToolTip(self, tooltip): + self.tooltip = tooltip + + def setMinimumHeight(self, height): + self.minimum_height = height + + def setStyleSheet(self, stylesheet): + self.stylesheet = stylesheet + + button = FakeButton() + + auto_routing_panel._style_command_button(button, "生成布线连接", "按导线任务布线") + + self.assertEqual("生成布线连接", button.text) + self.assertEqual("按导线任务布线", button.tooltip) + self.assertGreaterEqual(button.minimum_height, 28) + self.assertIn("color", button.stylesheet) + + def test_auto_routing_panel_status_highlights_auto_bridge_results(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="普通路径", - project_uuid="project-1", - kind="UserPath", + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "auto_diagnostic_bridges": {"created_count": 2}, + "auto_main_path_detour_bridges": { + "created_count": 1, + "rerouted": True, + "created_pair_labels": ["门板布线面 -> 主线槽A"], + }, + "auto_terminal_access_fallback_bridges": { + "created_count": 1, + "rerouted": True, + "created_pair_labels": ["安装板布线面 -> 柜内主路径"], + }, + } + + message = auto_routing_panel._format_route_panel_status(report) + + self.assertIn("自动补桥摘要:诊断桥 2 条,主路径补桥 1 条,端子接入补桥 1 条。", message) + self.assertIn("主路径配对:门板布线面 -> 主线槽A。", message) + self.assertIn("端子接入配对:安装板布线面 -> 柜内主路径。", message) + + def test_auto_routing_panel_auto_bridge_options_default_enabled_for_manual_testing(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + + self.assertTrue( + auto_routing_panel._panel_default_auto_bridge_enabled({}, "auto_create_diagnostic_bridges") ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="必经路径", - project_uuid="project-1", - kind="UserPath", + self.assertTrue( + auto_routing_panel._panel_default_auto_bridge_enabled({}, "auto_create_main_path_detour_bridges") + ) + self.assertTrue( + auto_routing_panel._panel_default_auto_bridge_enabled( + {}, + "auto_create_terminal_access_fallback_bridges", + ) + ) + self.assertFalse( + auto_routing_panel._panel_default_auto_bridge_enabled( + {"auto_create_diagnostic_bridges": False}, + "auto_create_diagnostic_bridges", + ) ) - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={ - "terminal_exit_length": 0.0, - "lane_spacing": 0.0, - "required_route_carrier_labels": ["必经路径"], - }, + def test_terminal_access_fallback_selection_status_includes_access_carriers(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + + message = auto_routing_panel._format_terminal_access_fallback_selection_status( + { + "selected_terminal_access_fallback_wires": 2, + "selected_terminal_access_fallback_targets": 1, + "selected_terminal_access_fallback_access_carriers": 1, + "selected_terminal_access_fallback_terminals": 1, + "selected_terminal_access_fallback_devices": 1, + } ) - labels = [ - segment["carrier"]["label"] - for segment in result["route_track"]["segments"] - ] - self.assertIn("必经路径", labels) - self.assertNotIn("普通路径", labels) - self.assertEqual( - ["必经路径"], - result["network"]["route_constraints"]["required"]["labels"], + self.assertIn("导线 2 条", message) + self.assertIn("目标 1 个", message) + self.assertIn("接入线 1 条", message) + self.assertIn("端子 1 个", message) + self.assertIn("设备 1 个", message) + + def test_wire_outside_boundary_selection_status_includes_route_refs(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + + message = auto_routing_panel._format_wire_outside_boundary_selection_status( + { + "selected_wire_outside_boundary_wires": 2, + "selected_wire_outside_boundary_route_carriers": 1, + "selected_wire_outside_boundary_route_sources": 1, + } ) - def test_eplan_connection_route_reports_unsatisfied_route_constraints(self): + self.assertIn("越界导线:2 条", message) + self.assertIn("路径 carrier 1 条", message) + self.assertIn("源对象 1 个", message) + + def test_eplan_connection_route_rejects_far_network_entry_to_avoid_huge_render_bbox(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -8123,326 +8948,258 @@ class AutoRoutingTest(unittest.TestCase): end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="普通路径", + [app.Vector(5000, 0, 20), app.Vector(5100, 0, 20)], project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - with self.assertRaises(auto_routing.AutoRoutingError) as context: - auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={ - "terminal_exit_length": 0.0, - "required_route_carrier_labels": ["不存在的必经路径"], - }, - ) - - self.assertIn("路径约束", str(context.exception)) + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals(doc, start, end) - def test_eplan_connection_route_chooses_clear_orthogonal_access_order(self): + def test_route_eplan_connection_between_terminals_fails_without_network(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() + app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(30, 30, 0), app.Vector(100, 30, 0)], - label="Only Duct", - project_uuid="project-1", - kind="WireDuct", - ) - obstacle = doc.addObject("Part::Feature", "AccessOrderObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(10, 20, -5, 5, -5, 5)) - - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, - ) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 30, 0)) - point_tuples = [(point.x, point.y, point.z) for point in result["points"]] - self.assertIn((0.0, 30.0, 0.0), point_tuples) - self.assertNotIn((30.0, 0.0, 0.0), point_tuples) - self.assertEqual(0, result["collision_count"]) + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals(doc, start, end) + self.assertEqual(0, len(wiring_objects.iter_routed_wire_objects(doc))) - def test_eplan_connection_route_marks_collision_warning_against_obstacle_bbox(self): + def test_surface_carrier_grid_uses_actual_rotated_face_plane(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], - project_uuid="project-1", - ) - obstacle = doc.addObject("Part::Feature", "Obstacle") - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) - parent = doc.addObject("App::Part", "DoorAssembly") - parent.Label = "FRONT DOOR-R ASS'Y" - parent.addObject(obstacle) + normal = app.Vector(0, 1, 1) + vertices = [ + app.Vector(0, 0, 0), + app.Vector(100, 0, 0), + app.Vector(0, 50, -50), + app.Vector(100, 50, -50), + ] + face = FakeFace( + FakeBoundBox(0, 100, 0, 50, -50, 0), + normal, + vertices=vertices, + center=app.Vector(50, 25, -25), + ) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + created = routing_network.create_surface_carriers_from_selection( + doc, + [FakeSelectionItem([face])], + project_uuid="project-1", + spacing=50.0, + offset=10.0, + margin=0.0, + ) - self.assertEqual("CollisionWarning", result["route_status"]) - self.assertEqual("CollisionWarning", result["wire"].RouteStatus) - self.assertEqual(1, result["collision_count"]) - self.assertEqual("HardIntersection", result["collisions"][0]["collision_kind"]) - self.assertEqual(["FRONT DOOR-R ASS'Y"], result["collisions"][0]["obstacle_parent_labels"]) - self.assertEqual(["DoorAssembly"], result["collisions"][0]["obstacle_parent_names"]) - self.assertEqual("1", result["wire"].QetRouteCollisionCount) - self.assertEqual("1", result["wire"].QetRouteHardIntersectionCount) - self.assertEqual("0", result["wire"].QetRouteClearanceWarningCount) - self.assertEqual("HardIntersectionWarning", result["wire"].QetRouteCollisionStatus) + self.assertGreater(len(created), 0) + first_point = created[0].Points[0] + for carrier in created: + for point in carrier.Points: + # The rotated face is y + z = 0; after a 10 mm normal offset, + # all generated points must stay on one parallel plane. + self.assertAlmostEqual(first_point.y + first_point.z, point.y + point.z, places=6) - def test_eplan_connection_route_locally_detours_terminal_access_around_third_party_device(self): + def test_route_path_creation_ignores_whole_solid_object_edges(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 100, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], - project_uuid="project-1", - kind="WireDuct", - ) - obstacle = doc.addObject("Part::Feature", "ThirdPartyDevice") - obstacle.Label = "第三方设备" - terminal_objects.ensure_string_property( - obstacle, - "QetElementUuid", - "QET Exchange", - "", - "device-obstacle", + solid = doc.addObject("Part::Feature", "CabinetSolid") + solid.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 100, 0, 10), + edges=[FakeEdge(app.Vector(0, 0, 0), app.Vector(100, 0, 0))], + faces=[object()], ) - obstacle.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) - result = auto_routing.route_eplan_connection_between_terminals( + created = routing_network.create_carriers_from_selection( doc, - start, - end, - options={ - "avoid_obstacles": False, - "avoid_local_access_obstacles": True, - "terminal_exit_length": 0.0, - }, - endpoint_metadata={ - "start_element_uuid": "device-start", - "end_element_uuid": "device-end", - }, + [FakeSelectionItem(obj=solid)], + project_uuid="project-1", ) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) - self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) + self.assertEqual([], created) - def test_network_route_limits_local_access_obstacles_to_nearby_bboxes(self): + def test_route_path_creation_splits_disconnected_shape_wires(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 100, 0)) - routing_network.create_route_carrier( + route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), + FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), + ], + ) + + created = routing_network.create_carriers_from_selection( doc, - [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], + [FakeSelectionItem(obj=route_path)], project_uuid="project-1", - kind="WireDuct", - ) - near_obstacle = doc.addObject("Part::Feature", "NearThirdPartyDevice") - near_obstacle.Label = "近处第三方设备" - terminal_objects.ensure_string_property( - near_obstacle, - "QetElementUuid", - "QET Exchange", - "", - "device-near-obstacle", ) - near_obstacle.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) - for index in range(120): - far_obstacle = doc.addObject("Part::Feature", "FarDevice{0}".format(index)) - far_obstacle.Shape = FakeShape( - FakeBoundBox(10000 + index * 20, 10010 + index * 20, 10000, 10010, 10000, 10010) - ) - - calls = {"count": 0} - original_segment_intersects_bbox = auto_routing._segment_intersects_bbox - - def counted_segment_intersects_bbox(start_point, end_point, bbox): - calls["count"] += 1 - return original_segment_intersects_bbox(start_point, end_point, bbox) - - auto_routing._segment_intersects_bbox = counted_segment_intersects_bbox - try: - result = auto_routing.build_network_route( - start, - end, - options={ - "avoid_obstacles": False, - "avoid_local_access_obstacles": True, - "terminal_exit_length": 0.0, - }, - doc=doc, - ) - finally: - auto_routing._segment_intersects_bbox = original_segment_intersects_bbox - self.assertIsNotNone(result) - self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) - self.assertLess(calls["count"], 80) + self.assertEqual(2, len(created)) + self.assertEqual( + [ + [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0)], + [(80.0, 80.0, 20.0), (120.0, 80.0, 20.0)], + ], + [[(point.x, point.y, point.z) for point in carrier.Points] for carrier in created], + ) - def test_network_route_ignores_unbound_structural_bboxes_for_local_access_avoidance(self): + def test_route_path_creation_projects_line_to_selected_face(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 100, 0)) - routing_network.create_route_carrier( + face = FakeFace( + FakeBoundBox(0, 100, 0, 100, 0, 0), + app.Vector(0, 0, 1), + ) + draft_line = doc.addObject("Part::Feature", "DraftLine") + draft_line.Shape = FakeShape( + FakeBoundBox(10, 90, 10, 90, 25, 35), + edges=[FakeEdge(app.Vector(10, 10, 25), app.Vector(90, 90, 35))], + ) + + created = routing_network.create_carriers_from_selection( doc, - [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], + [ + FakeSelectionItem([face]), + FakeSelectionItem(obj=draft_line), + ], project_uuid="project-1", - kind="WireDuct", - ) - near_device = doc.addObject("Part::Feature", "BoundNearDevice") - terminal_objects.ensure_string_property( - near_device, - "QetElementUuid", - "QET Exchange", - "", - "device-near-obstacle", ) - near_device.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) - for index in range(80): - cabinet_part = doc.addObject("Part::Feature", "ImportedCabinetPart{0}".format(index)) - cabinet_part.Shape = FakeShape(FakeBoundBox(-1000, 1000, -1000, 1000, -1000, 1000)) - calls = {"count": 0} - original_segment_intersects_bbox = auto_routing._segment_intersects_bbox + self.assertEqual(1, len(created)) + self.assertEqual([2.0, 2.0], [point.z for point in created[0].Points]) - def counted_segment_intersects_bbox(start_point, end_point, bbox): - calls["count"] += 1 - return original_segment_intersects_bbox(start_point, end_point, bbox) + def test_wire_duct_entity_generates_centerline_and_marks_source_pass_through(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + duct = doc.addObject("Part::Feature", "WireDuct") + duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - auto_routing._segment_intersects_bbox = counted_segment_intersects_bbox - try: - result = auto_routing.build_network_route( - start, - end, - options={ - "avoid_obstacles": False, - "avoid_local_access_obstacles": True, - "terminal_exit_length": 0.0, - }, - doc=doc, - ) - finally: - auto_routing._segment_intersects_bbox = original_segment_intersects_bbox + created = routing_network.create_wire_duct_carriers_from_selection( + doc, + [FakeSelectionItem(obj=duct)], + project_uuid="project-1", + margin=20.0, + ) - self.assertIsNotNone(result) - self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) - self.assertLess(calls["count"], 80) + self.assertEqual(3, len(created)) + carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0] + open_ends = [item for item in created if item.QetRouteCarrierKind == "WireDuctOpenEnd"] + self.assertEqual("WireDuct", carrier.QetRouteCarrierKind) + self.assertEqual(2, len(open_ends)) + self.assertEqual("PassThrough", duct.QetRoutingObstacleMode) + self.assertEqual([(20.0, 0.0, 15.0), (100.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points]) - def test_network_route_caps_extra_entry_candidates_in_batch_mode(self): + def test_wire_duct_source_end_margin_controls_generated_centerline_length(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - for index in range(8): - routing_network.create_route_carrier( - doc, - [app.Vector(0, index * 10, 50), app.Vector(100, index * 10, 50)], - project_uuid="project-1", - kind="WireDuct", - ) - obstacle = doc.addObject("Part::Feature", "BoundNearDevice") - terminal_objects.ensure_string_property( - obstacle, - "QetElementUuid", - "QET Exchange", - "", - "device-near-obstacle", + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "线槽A" + duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) + duct.QetWireDuctEndMarginMm = 5.0 + + created = routing_network.create_wire_duct_carriers_from_document( + doc, + project_uuid="project-1", ) - obstacle.Shape = FakeShape(FakeBoundBox(15, 25, -5, 5, 40, 60)) - calls = {"shortest_path": 0} - original_shortest_path = routing_network.shortest_path_with_carriers - def counted_shortest_path(*args, **kwargs): - calls["shortest_path"] += 1 - return original_shortest_path(*args, **kwargs) + carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0] + self.assertIn("QetWireDuctEndMarginMm", duct.PropertiesList) + self.assertEqual(5.0, duct.QetWireDuctEndMarginMm) + self.assertEqual([(5.0, 0.0, 15.0), (115.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points]) - routing_network.shortest_path_with_carriers = counted_shortest_path - try: - result = auto_routing.build_network_route( - start, - end, - options={ - "network_entry_candidate_limit": 3, - "network_entry_candidate_total_limit": 4, - "avoid_obstacles": False, - "avoid_local_access_obstacles": True, - "terminal_exit_length": 0.0, - }, - doc=doc, - ) - finally: - routing_network.shortest_path_with_carriers = original_shortest_path + def test_wire_duct_source_capacity_is_copied_to_generated_carriers(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + duct = doc.addObject("Part::Feature", "WireDuct") + duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) + duct.QetRouteCarrierCapacity = 4 - self.assertIsNotNone(result) - self.assertLessEqual(calls["shortest_path"], 16) + created = routing_network.create_wire_duct_carriers_from_selection( + doc, + [FakeSelectionItem(obj=duct)], + project_uuid="project-1", + margin=20.0, + ) - def test_eplan_connection_route_marks_clearance_warning_against_expanded_obstacle_bbox(self): + self.assertIn("QetRouteCarrierCapacity", duct.PropertiesList) + self.assertTrue(all(item.QetRouteCarrierCapacity == 4 for item in created)) + + def test_auto_detect_wire_ducts_ignores_cabinet_models(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "线槽A" + duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) + cabinet = doc.addObject("Part::Feature", "Cabinet") + cabinet.Label = "3D机柜" + cabinet.Shape = FakeShape(FakeBoundBox(0, 300, 0, 80, 0, 400)) + + created = routing_network.create_wire_duct_carriers_from_document( + doc, + project_uuid="project-1", + ) + created_again = routing_network.create_wire_duct_carriers_from_document( + doc, + project_uuid="project-1", + ) + + self.assertEqual(3, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual(1, len([item for item in created if item.QetRouteCarrierKind == "WireDuct"])) + self.assertEqual(2, len([item for item in created if item.QetRouteCarrierKind == "WireDuctOpenEnd"])) + self.assertEqual("PassThrough", duct.QetRoutingObstacleMode) + self.assertFalse(hasattr(cabinet, "QetRoutingObstacleMode")) + + def test_wire_duct_source_is_not_reported_as_collision(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuct") + duct.Shape = FakeShape(FakeBoundBox(-10, 130, -10, 10, 15, 25)) + routing_network.create_wire_duct_carriers_from_selection( doc, - [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], - label="主线槽A", + [FakeSelectionItem(obj=duct)], project_uuid="project-1", + margin=0.0, ) - obstacle = doc.addObject("Part::Feature", "NearObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 3, 6, 90, 110)) - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"obstacle_clearance": 5.0}, - ) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual("CollisionWarning", result["route_status"]) - self.assertEqual(1, result["collision_count"]) - self.assertEqual("ClearanceWarning", result["collisions"][0]["collision_kind"]) - self.assertEqual(3.0, result["collisions"][0]["obstacle_bbox"]["ymin"]) - self.assertEqual(-2.0, result["collisions"][0]["collision_bbox"]["ymin"]) - self.assertEqual("1", result["wire"].QetRouteCollisionCount) - self.assertEqual("0", result["wire"].QetRouteHardIntersectionCount) - self.assertEqual("1", result["wire"].QetRouteClearanceWarningCount) - self.assertEqual("ClearanceWarning", result["wire"].QetRouteCollisionStatus) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) - def test_eplan_connection_route_ignores_terminal_exit_segment_collision(self): + def test_eplan_connection_route_uses_alternate_carrier_to_avoid_obstacle(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -8454,107 +9211,156 @@ class AutoRoutingTest(unittest.TestCase): doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", + kind="WireDuct", ) - terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") - terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) + routing_network.create_route_carrier( + doc, + [ + app.Vector(0, 0, 20), + app.Vector(0, 50, 20), + app.Vector(100, 50, 20), + app.Vector(100, 0, 20), + ], + project_uuid="project-1", + kind="WireDuct", + ) + obstacle = doc.addObject("Part::Feature", "CabinetObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 15, 25)) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) + self.assertTrue(result["network"]["obstacle_aware"]) + self.assertGreaterEqual(result["network"]["blocked_segments"], 1) + self.assertIn(50.0, [point.y for point in result["points"]]) - def test_eplan_connection_route_ignores_explicit_start_local_route_collision(self): + def test_eplan_connection_route_prefers_entry_candidate_without_access_collision(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) - start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") - start.QetTerminalLocalRoutePointsJson = json.dumps( - [[0, 0, 0], [20, 0, 0], [20, 40, 0]] + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(20, 0, 0), app.Vector(100, 0, 0)], + label="Near Duct", + project_uuid="project-1", + kind="WireDuct", ) routing_network.create_route_carrier( doc, - [app.Vector(20, 80, 0), app.Vector(120, 80, 0)], - label="Cabinet Main Path", + [app.Vector(0, 30, 0), app.Vector(100, 30, 0)], + label="Clear Duct", project_uuid="project-1", + kind="WireDuct", ) - local_body = doc.addObject("Part::Feature", "StartDeviceLocalShell") - local_body.Shape = FakeShape(FakeBoundBox(15, 25, 15, 25, -5, 5)) + obstacle = doc.addObject("Part::Feature", "AccessObstacle") + obstacle.Label = "Access Obstacle" + obstacle.Shape = FakeShape(FakeBoundBox(10, 15, -5, 5, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) - self.assertEqual("Routed", result["route_status"]) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Clear Duct", labels) + self.assertNotIn("Near Duct", labels) self.assertEqual(0, result["collision_count"]) - diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) - self.assertEqual(3, len(diagnostics["endpoint_access"]["start_points"])) - def test_eplan_connection_route_still_reports_main_path_collision_after_local_route(self): + def test_eplan_connection_route_keeps_clear_access_candidates_beyond_distance_limit(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) - start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") - start.QetTerminalLocalRoutePointsJson = json.dumps( - [[0, 0, 0], [20, 0, 0], [20, 40, 0]] - ) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + for index in range(9): + routing_network.create_route_carrier( + doc, + [app.Vector(20, index, 0), app.Vector(100, index, 0)], + label="Near Blocked Duct {0}".format(index + 1), + project_uuid="project-1", + kind="WireDuct", + ) routing_network.create_route_carrier( doc, - [app.Vector(20, 80, 0), app.Vector(120, 80, 0)], - label="Cabinet Main Path", + [app.Vector(0, 30, 0), app.Vector(100, 30, 0)], + label="Clear Duct", project_uuid="project-1", + kind="WireDuct", ) - main_obstacle = doc.addObject("Part::Feature", "MainPathObstacle") - main_obstacle.Shape = FakeShape(FakeBoundBox(55, 65, 75, 85, -5, 5)) + obstacle = doc.addObject("Part::Feature", "AccessObstacle") + obstacle.Label = "Access Obstacle" + obstacle.Shape = FakeShape(FakeBoundBox(10, 15, -5, 20, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) - self.assertEqual("CollisionWarning", result["route_status"]) - self.assertEqual(1, result["collision_count"]) - self.assertEqual("MainPathObstacle", result["collisions"][0]["obstacle_name"]) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Clear Duct", labels) + self.assertTrue(all(not label.startswith("Near Blocked Duct") for label in labels)) + self.assertEqual(0, result["network"]["route_candidate_obstacle_hits"]) - def test_eplan_connection_route_detours_local_access_segment_around_obstacle(self): + def test_eplan_connection_route_prefers_carrier_inside_cabinet_boundary(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 49, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], - label="主线槽A", + [app.Vector(0, 51, 0), app.Vector(100, 51, 0)], + label="Outside Cabinet Path", project_uuid="project-1", + kind="UserPath", ) - obstacle = doc.addObject("Part::Feature", "AccessObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, 40, 60)) - - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + routing_network.create_route_carrier( + doc, + [app.Vector(0, -40, 0), app.Vector(100, -40, 0)], + label="Inside Cabinet Path", + project_uuid="project-1", + kind="UserPath", + ) + boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + boundary.QetRoutingBoundaryKind = "CabinetInterior" - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) - self.assertTrue( - any(abs(point.x) > 5.0 or abs(point.y) > 5.0 for point in result["points"]), - "局部接入段应增加侧向绕障拐点,而不是直接穿过障碍盒。", + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) - def test_eplan_connection_route_ignores_endpoint_device_body_as_obstacle(self): + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Inside Cabinet Path", labels) + self.assertNotIn("Outside Cabinet Path", labels) + self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) + + def test_eplan_connection_route_prefers_inside_detour_over_shorter_outside_shortcut(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -8562,666 +9368,490 @@ class AutoRoutingTest(unittest.TestCase): terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - device = doc.addObject("App::DocumentObjectGroup", "QETDeviceStart") - device.QetInstanceId = start.QetInstanceId - device.addObject(start) - body = doc.addObject("Part::Feature", "StartDeviceBody") - body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) - device.addObject(body) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 51, 0), app.Vector(100, 0, 0)], + label="Outside Shortcut", project_uuid="project-1", + kind="UserPath", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(0, -40, 0), app.Vector(100, -40, 0), app.Vector(100, 0, 0)], + label="Inside Cabinet Detour", + project_uuid="project-1", + kind="UserPath", ) + boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + boundary.QetRoutingBoundaryKind = "CabinetInterior" - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Inside Cabinet Detour", labels) + self.assertNotIn("Outside Shortcut", labels) + self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) - def test_route_eplan_connections_from_payload_skips_missing_terminal(self): + def test_eplan_connection_wire_records_boundary_warning_when_route_leaves_cabinet(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - payload = { - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-missing", - "end_element_uuid": "device-missing", - "end_instance_id": "instance-missing", - "end_terminal_display": "A1", - } - ] - } + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 49, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 60, 0), app.Vector(100, 60, 0)], + label="Only Outside Cabinet Path", + project_uuid="project-1", + kind="UserPath", + ) + boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + boundary.QetRoutingBoundaryKind = "CabinetInterior" - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_terminal"]) - self.assertEqual(1, report["available_terminals"]) - self.assertEqual(0, report["local_terminals"]) - self.assertEqual(["terminal-missing"], report["missing_endpoint_uuids"]) - self.assertEqual("terminal-start", report["missing_endpoint_samples"][0]["start_terminal_uuid"]) - self.assertTrue(report["missing_endpoint_samples"][0]["start_found"]) - self.assertFalse(report["missing_endpoint_samples"][0]["end_found"]) - self.assertEqual("instance-missing", report["missing_endpoint_samples"][0]["end_instance_id"]) - self.assertEqual("A1", report["missing_endpoint_samples"][0]["end_terminal_display"]) - self.assertEqual(0, report["missing_endpoint_samples"][0]["end_element_terminal_count"]) - self.assertEqual([], report["missing_endpoint_samples"][0]["end_element_terminal_samples"]) - self.assertEqual(0, report["missing_endpoint_samples"][0]["end_instance_terminal_count"]) - self.assertEqual([], report["missing_endpoint_samples"][0]["end_instance_terminal_samples"]) + self.assertGreater(result["network"]["route_candidate_boundary_violations"], 0) + self.assertTrue(result["wire"].QetRouteBoundaryAware) + self.assertEqual("BoundaryWarning", result["wire"].QetRouteBoundaryStatus) self.assertEqual( - "device_not_in_3d_scene", - report["missing_endpoint_samples"][0]["end_missing_endpoint_reason_code"], + str(result["network"]["route_candidate_boundary_violations"]), + result["wire"].QetRouteBoundaryViolationCount, ) - self.assertIn("终点 element=device-missing, instance=instance-missing, terminal=A1", message) - self.assertIn("原因=该 2D 设备未在 FreeCAD 场景中找到", message) - def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_payload_devices(self): + def test_eplan_connection_wire_records_long_network_access_warning(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - payload = { - "devices": [ - { - "element_uuid": "device-missing", - "instance_id": "instance-from-device-list", - "display_tag": "UD:8", - "terminals": [ - { - "terminal_uuid": "device-missing:terminal-a", - "terminal_display": "A1", - } - ], - } - ], - "wires": [ - { - "wire_id": "wire-1", - "wire_mark": "N-MISS", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "device-missing:terminal-a", - "end_element_uuid": "device-missing", - "end_terminal_display": "A1", - } - ], - } - - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) - sample = report["missing_endpoint_samples"][0] + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 125, 0), app.Vector(100, 125, 0)], + label="Far Cabinet Main Path", + project_uuid="project-1", + kind="UserPath", + ) - self.assertEqual("instance-from-device-list", sample["end_instance_id"]) - self.assertEqual("UD:8", sample["end_device_label"]) - self.assertEqual( - {"device_not_in_3d_scene": 1}, - report["missing_terminal_summary"]["reason_code_counts"], + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "terminal_exit_length": 0.0, + "lane_spacing": 0.0, + "terminal_access_warning_distance": 50.0, + }, ) - self.assertEqual(1, len(report["missing_terminal_summary"]["device_groups"])) - self.assertEqual("UD:8", report["missing_terminal_summary"]["device_groups"][0]["device_label"]) - self.assertEqual(["A1"], report["missing_terminal_summary"]["device_groups"][0]["terminal_displays"]) - self.assertIn("UD:8", message) - self.assertIn("需补端子设备:UD:8 缺 1 处(A1)", message) - self.assertIn("instance=instance-from-device-list", message) - def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_context_json_devices(self): + wire = result["wire"] + self.assertEqual("125.000", wire.QetRouteEntryDistanceMm) + self.assertEqual("125.000", wire.QetRouteExitDistanceMm) + self.assertEqual("node", wire.QetRouteEntryPointMode) + self.assertEqual("node", wire.QetRouteExitPointMode) + self.assertEqual("1", wire.QetRouteEntryCandidateRank) + self.assertEqual("1", wire.QetRouteExitCandidateRank) + self.assertEqual("50.000", wire.QetRouteAccessWarningDistanceMm) + self.assertEqual("LongAccessWarning", wire.QetRouteAccessStatus) + self.assertEqual("entry,exit", wire.QetRouteAccessWarningSides) + payload = json.loads(wire.QetRouteDiagnosticsJson) + self.assertEqual("LongAccessWarning", payload["access"]["access_status"]) + self.assertEqual(["entry", "exit"], payload["access"]["warning_sides"]) + + def test_eplan_connection_route_keeps_inside_boundary_candidates_beyond_distance_limit(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - with tempfile.TemporaryDirectory() as temp_dir: - json_path = Path(temp_dir) / "2d_to_3d.json" - json_path.write_text( - json.dumps( - { - "project_uuid": "project-1", - "devices": [ - { - "element_uuid": "device-missing", - "instance_id": "instance-from-context-json", - "display_tag": "UD:8", - } - ], - "wires": [ - { - "wire_id": "wire-1", - "wire_mark": "N-MISS", - "wire_style_id": "1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "device-missing:terminal-a", - "end_element_uuid": "device-missing", - "end_terminal_display": "A1", - } - ], - }, - ensure_ascii=False, - ), - encoding="utf-8", + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 49, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) + for index in range(9): + y = 51 + index + routing_network.create_route_carrier( + doc, + [app.Vector(0, y, 0), app.Vector(100, y, 0)], + label="Outside Candidate {0}".format(index + 1), + project_uuid="project-1", + kind="UserPath", ) - app._qet_exchange_summary = {"json_path": str(json_path)} - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "wire_mark": "N-MISS", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "device-missing:terminal-a", - "end_element_uuid": "device-missing", - "end_terminal_display": "A1", - } - ], - } - - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + routing_network.create_route_carrier( + doc, + [app.Vector(0, -40, 0), app.Vector(100, -40, 0)], + label="Inside Cabinet Path", + project_uuid="project-1", + kind="UserPath", + ) + boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + boundary.QetRoutingBoundaryKind = "CabinetInterior" - sample = report["missing_endpoint_samples"][0] - self.assertEqual("instance-from-context-json", sample["end_instance_id"]) - self.assertEqual("UD:8", sample["end_device_label"]) - self.assertEqual( - "instance-from-context-json", - report["missing_terminal_summary"]["device_groups"][0]["instance_id"], + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) - self.assertTrue(report["context_devices_loaded"]) - self.assertEqual(1, report["context_device_count"]) - self.assertEqual(str(json_path), report["context_devices_json_path"]) - def test_route_eplan_connections_from_payload_reports_device_without_terminals(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - device = doc.addObject("App::DocumentObjectGroup", "QETDevice_without_terminals") - terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-no-terminals") - terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-no-terminals") - payload = { - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-missing", - "end_element_uuid": "device-no-terminals", - "end_instance_id": "instance-no-terminals", - "end_terminal_display": "A1", - } - ] - } - - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) - - sample = report["missing_endpoint_samples"][0] - self.assertEqual("QETDevice_without_terminals", sample["end_device_name"]) - self.assertTrue(sample["end_device_in_scene"]) - self.assertEqual("no_3d_terminals_for_element", sample["end_missing_endpoint_reason_code"]) - self.assertIn("原因=该 2D 设备在 FreeCAD 中没有工程端子", message) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Inside Cabinet Path", labels) + self.assertTrue(all(not label.startswith("Outside Candidate") for label in labels)) + self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) - def test_route_eplan_connections_from_payload_reports_missing_device_binding_metadata(self): + def test_eplan_connection_route_tolerates_missing_route_constraint_collector(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - payload = { - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-missing", - "end_terminal_display": "A1", - } - ] - } - - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="主路径", + project_uuid="project-1", + kind="UserPath", + ) + collector = routing_network.collect_route_constraint_options + delattr(routing_network, "collect_route_constraint_options") + try: + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + finally: + routing_network.collect_route_constraint_options = collector - sample = report["missing_endpoint_samples"][0] - self.assertEqual("missing_device_binding_metadata", sample["end_missing_endpoint_reason_code"]) - self.assertEqual("导线端点缺少 2D/3D 设备绑定信息", sample["end_missing_endpoint_reason_label"]) - self.assertIn("QET 导线端点缺少 element_uuid", message) - self.assertIn("第一版不要求 start/end_instance_id", message) - self.assertIn("原因=导线端点缺少 2D/3D 设备绑定信息", message) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual({}, result["network"].get("route_constraints", {})) - def test_route_eplan_connections_from_payload_applies_per_wire_required_route(self): + def test_eplan_connection_route_avoids_forbidden_carrier_label(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="普通路径", + label="禁止路径", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="必经路径", + label="允许路径", project_uuid="project-1", kind="UserPath", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-required", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - "required_route_carrier_labels": ["必经路径"], - } - ], - } - report = auto_routing.route_eplan_connections_from_payload( + result = auto_routing.route_eplan_connection_between_terminals( doc, - payload, - options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + start, + end, + options={ + "terminal_exit_length": 0.0, + "lane_spacing": 0.0, + "forbidden_route_carrier_labels": ["禁止路径"], + }, ) labels = [ segment["carrier"]["label"] - for segment in report["routes"][0]["route_track"]["segments"] + for segment in result["route_track"]["segments"] ] - self.assertIn("必经路径", labels) - self.assertNotIn("普通路径", labels) + self.assertIn("允许路径", labels) + self.assertNotIn("禁止路径", labels) - def test_route_eplan_connections_from_payload_applies_per_wire_required_source_name(self): + def test_eplan_connection_route_avoids_carrier_marked_forbidden(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - direct = routing_network.create_route_carrier( + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="普通路径", + label="近路径", project_uuid="project-1", kind="UserPath", ) - direct.QetRouteSourceName = "NormalSketch" - required = routing_network.create_route_carrier( + forbidden.QetRouteConstraintMode = "Forbidden" + routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="黄色主路径", + label="远路径", project_uuid="project-1", kind="UserPath", ) - required.QetRouteSourceName = "RequiredSketch" - required.QetRouteSourceLabel = "黄色主路径草图" - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-required-source", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - "required_route_carrier_source_names": ["RequiredSketch"], - } - ], - } - report = auto_routing.route_eplan_connections_from_payload( + result = auto_routing.route_eplan_connection_between_terminals( doc, - payload, + start, + end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] - for segment in report["routes"][0]["route_track"]["segments"] + for segment in result["route_track"]["segments"] ] - self.assertIn("黄色主路径", labels) - self.assertNotIn("普通路径", labels) - self.assertEqual( - ["RequiredSketch"], - report["routes"][0]["network"]["route_constraints"]["required"]["source_names"], + self.assertIn("远路径", labels) + self.assertNotIn("近路径", labels) + self.assertIn( + forbidden.Name, + result["network"]["route_constraints"]["forbidden"]["names"], ) - def test_route_eplan_connections_from_payload_applies_per_wire_forbidden_route(self): + def test_eplan_connection_route_accepts_chinese_constraint_mode_aliases(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="禁止路径", + label="近路径", project_uuid="project-1", kind="UserPath", ) + forbidden.QetRouteConstraintMode = "禁止经过" routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="允许路径", + label="远路径", project_uuid="project-1", kind="UserPath", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-forbidden", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - "forbidden_route_carrier_labels": ["禁止路径"], - } - ], - } - report = auto_routing.route_eplan_connections_from_payload( + result = auto_routing.route_eplan_connection_between_terminals( doc, - payload, + start, + end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] - for segment in report["routes"][0]["route_track"]["segments"] + for segment in result["route_track"]["segments"] ] - self.assertIn("允许路径", labels) - self.assertNotIn("禁止路径", labels) + self.assertIn("远路径", labels) + self.assertNotIn("近路径", labels) + self.assertIn( + forbidden.Name, + result["network"]["route_constraints"]["forbidden"]["names"], + ) - def test_route_eplan_connections_from_payload_applies_per_wire_forbidden_source_name(self): + def test_eplan_connection_route_uses_carrier_marked_required(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - forbidden = routing_network.create_route_carrier( + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="禁止源路径", + label="近路径", project_uuid="project-1", kind="UserPath", ) - forbidden.QetRouteSourceName = "ForbiddenSketch" - allowed = routing_network.create_route_carrier( + required = routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="允许源路径", + label="远路径", project_uuid="project-1", kind="UserPath", ) - allowed.QetRouteSourceName = "AllowedSketch" - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-forbidden-source", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - "forbidden_route_carrier_source_names": ["ForbiddenSketch"], - } - ], - } + required.QetRouteConstraintMode = "Required" - report = auto_routing.route_eplan_connections_from_payload( + result = auto_routing.route_eplan_connection_between_terminals( doc, - payload, + start, + end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] - for segment in report["routes"][0]["route_track"]["segments"] + for segment in result["route_track"]["segments"] ] - self.assertIn("允许源路径", labels) - self.assertNotIn("禁止源路径", labels) - self.assertEqual( - ["ForbiddenSketch"], - report["routes"][0]["network"]["route_constraints"]["forbidden"]["source_names"], + self.assertIn("远路径", labels) + self.assertNotIn("近路径", labels) + self.assertIn( + required.Name, + result["network"]["route_constraints"]["required"]["names"], ) - def test_route_eplan_connections_from_payload_classifies_unsatisfied_route_constraints(self): + def test_source_required_constraint_from_multi_wire_sketch_accepts_one_generated_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + route_path = doc.addObject("Sketcher::SketchObject", "YellowMainRouteSketch") + route_path.Label = "黄色主路径" + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + wires=[ + FakeWire([app.Vector(0, 0, 20), app.Vector(100, 0, 20)]), + FakeWire([app.Vector(0, 80, 20), app.Vector(100, 80, 20)]), + ], + ) + selection = [FakeSelectionItem(obj=route_path)] + routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") + carriers = routing_network.create_user_path_carriers_from_selection( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="普通路径", + selection, project_uuid="project-1", - kind="UserPath", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-unsatisfied", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - "required_route_carrier_labels": ["不存在的必经路径"], - } - ], - } - report = auto_routing.route_eplan_connections_from_payload( + result = auto_routing.route_eplan_connection_between_terminals( doc, - payload, - options={"terminal_exit_length": 0.0}, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) - self.assertIn("路径约束", report["missing_route_network_samples"][0]["error"]) - - def test_route_eplan_connections_from_payload_skips_resolved_tasks_without_route_network(self): - _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-{0}".format(index), - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } - for index in range(3) - ], - } - original_route = auto_routing.route_eplan_connection_between_terminals - - def fail_if_called(*_args, **_kwargs): - raise AssertionError("batch route must not call per-wire routing without route carriers") - - auto_routing.route_eplan_connection_between_terminals = fail_if_called - try: - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - finally: - auto_routing.route_eplan_connection_between_terminals = original_route - - self.assertEqual(0, report["routed"]) - self.assertEqual(3, report["skipped_missing_route_network"]) - self.assertEqual(3, report["route_status_counts"]["MissingRouteNetwork"]) - self.assertEqual([], report["errors"]) - self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) - - def test_route_eplan_connection_tasks_marks_task_missing_route_network_when_skipped(self): - _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - task = wiring_objects.create_wire_task( - doc, - "project-1", - "wire-missing-network", - "N1", - "terminal-start", - "terminal-end", - "instance-a", - "instance-b", + route_carrier_names = [ + segment["carrier"]["name"] + for segment in result["route_track"]["segments"] + if not segment.get("is_bridge") + ] + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertIn(carriers[0].Name, route_carrier_names) + self.assertNotIn(carriers[1].Name, route_carrier_names) + self.assertEqual( + ["黄色主路径"], + result["network"]["route_constraints"]["required"]["source_labels"], ) - task.RouteStatus = "Routed" - - report = auto_routing.route_eplan_connection_tasks(doc) - - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertEqual("MissingRouteNetwork", task.RouteStatus) - def test_route_eplan_connection_tasks_auto_creates_diagnostic_bridge_before_routing(self): + def test_eplan_connection_route_requires_carrier_label(self): _install_fake_freecad() - terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="普通路径", project_uuid="project-1", - kind="RoutingRange", - label="安装板兜底路径", + kind="UserPath", ) - wiring_objects.create_wire_task( + routing_network.create_route_carrier( doc, - "project-1", - "wire-task-bridge", - "N1", - "terminal-start", - "terminal-end", - "instance-a", - "instance-b", + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="必经路径", + project_uuid="project-1", + kind="UserPath", ) - original_diagnostic = routing_network.diagnose_routing_path_network - original_create = routing_network.create_user_path_bridges_from_diagnostic_suggestions - calls = {"diagnostic": 0} - - def fake_diagnostic(*_args, **_kwargs): - calls["diagnostic"] += 1 - if calls["diagnostic"] == 1: - return { - "ok": False, - "issues": [ - { - "severity": "warning", - "code": "wire_ducts_without_terminal_access", - "count": 1, - }, - ], - "summary": {"carriers": 1}, - "wire_ducts_without_terminal_access": [ - { - "index": 0, - "carrier_names": ["孤立线槽"], - "bridge_suggestion": {"distance_mm": 40.0}, - }, - ], - } - return {"ok": True, "issues": [], "summary": {"carriers": 2}} - - def fake_create(_doc, _diagnostic, project_uuid=""): - carrier = routing_network.create_route_carrier( - _doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid=project_uuid or "project-1", - kind="WireDuct", - label="诊断桥接后主路径", - ) - return {"suggestions": 1, "created": [carrier], "duplicates": 0, "stale_suggestions": 0} - routing_network.diagnose_routing_path_network = fake_diagnostic - routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create - try: - report = auto_routing.route_eplan_connection_tasks( - doc, - options={"auto_create_diagnostic_bridges": True}, - ) - finally: - routing_network.diagnose_routing_path_network = original_diagnostic - routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "terminal_exit_length": 0.0, + "lane_spacing": 0.0, + "required_route_carrier_labels": ["必经路径"], + }, + ) - self.assertEqual(1, report["auto_diagnostic_bridges"]["created_count"]) - self.assertEqual({"main_path_routes": 1, "fallback_routes": 0}, report["route_path_usage"]) - self.assertEqual(["Routed"], list(report["route_status_counts"].keys())) - self.assertNotIn("main_path_not_used", report["issue_codes"]) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("必经路径", labels) + self.assertNotIn("普通路径", labels) + self.assertEqual( + ["必经路径"], + result["network"]["route_constraints"]["required"]["labels"], + ) - def test_eplan_connection_route_prefers_wire_duct_over_shorter_routing_range(self): + def test_eplan_connection_route_reports_unsatisfied_route_constraints(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(300, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(300, 0, 20)], - project_uuid="project-1", - kind="RoutingRange", - ) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [ - app.Vector(0, 0, 20), - app.Vector(0, 1200, 20), - app.Vector(300, 1200, 20), - app.Vector(300, 0, 20), - ], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="普通路径", project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + with self.assertRaises(auto_routing.AutoRoutingError) as context: + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "terminal_exit_length": 0.0, + "required_route_carrier_labels": ["不存在的必经路径"], + }, + ) - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + self.assertIn("路径约束", str(context.exception)) - def test_eplan_connection_wire_records_fallback_route_quality_warning(self): + def test_eplan_connection_route_chooses_clear_orthogonal_access_order(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], - label="安装板兜底路径", + [app.Vector(30, 30, 0), app.Vector(100, 30, 0)], + label="Only Duct", project_uuid="project-1", - kind="RoutingRange", + kind="WireDuct", ) + obstacle = doc.addObject("Part::Feature", "AccessOrderObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(10, 20, -5, 5, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, @@ -9230,35 +9860,59 @@ class AutoRoutingTest(unittest.TestCase): options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) - wire = result["wire"] - self.assertEqual("FallbackPathWarning", wire.QetRouteQualityStatus) - self.assertEqual("RoutingRange", wire.QetRouteFallbackCarrierKinds) - self.assertEqual("安装板兜底路径", wire.QetRouteFallbackCarrierLabels) - self.assertEqual("route_quality_warnings", wire.QetRouteIssueCodes) - self.assertEqual("路径质量告警", wire.QetRouteIssueLabels) - payload = json.loads(wire.QetRouteDiagnosticsJson) - self.assertEqual(["route_quality_warnings"], payload["issue_codes"]) - self.assertEqual(["路径质量告警"], payload["issue_labels"]) - self.assertEqual("FallbackPathWarning", payload["quality"]["quality_status"]) - self.assertEqual(["RoutingRange"], payload["quality"]["fallback_carrier_kinds"]) + point_tuples = [(point.x, point.y, point.z) for point in result["points"]] + self.assertIn((0.0, 30.0, 0.0), point_tuples) + self.assertNotIn((30.0, 0.0, 0.0), point_tuples) + self.assertEqual(0, result["collision_count"]) - def test_eplan_connection_wire_records_third_party_collision_issue(self): + def test_eplan_connection_route_marks_collision_warning_against_obstacle_bbox(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], - label="线槽主路径", + [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], + project_uuid="project-1", + ) + obstacle = doc.addObject("Part::Feature", "Obstacle") + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) + parent = doc.addObject("App::Part", "DoorAssembly") + parent.Label = "FRONT DOOR-R ASS'Y" + parent.addObject(obstacle) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual("CollisionWarning", result["route_status"]) + self.assertEqual("CollisionWarning", result["wire"].RouteStatus) + self.assertEqual(1, result["collision_count"]) + self.assertEqual("HardIntersection", result["collisions"][0]["collision_kind"]) + self.assertEqual(["FRONT DOOR-R ASS'Y"], result["collisions"][0]["obstacle_parent_labels"]) + self.assertEqual(["DoorAssembly"], result["collisions"][0]["obstacle_parent_names"]) + self.assertEqual("1", result["wire"].QetRouteCollisionCount) + self.assertEqual("1", result["wire"].QetRouteHardIntersectionCount) + self.assertEqual("0", result["wire"].QetRouteClearanceWarningCount) + self.assertEqual("HardIntersectionWarning", result["wire"].QetRouteCollisionStatus) + + def test_eplan_connection_route_locally_detours_terminal_access_around_third_party_device(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "ThirdPartyDevice") - obstacle.Label = "设备A" + obstacle.Label = "第三方设备" terminal_objects.ensure_string_property( obstacle, "QetElementUuid", @@ -9266,101 +9920,137 @@ class AutoRoutingTest(unittest.TestCase): "", "device-obstacle", ) - obstacle.Shape = FakeShape(FakeBoundBox(50, 70, -10, 10, 15, 25)) + obstacle.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + options={ + "avoid_obstacles": False, + "avoid_local_access_obstacles": True, + "terminal_exit_length": 0.0, + }, endpoint_metadata={ "start_element_uuid": "device-start", "end_element_uuid": "device-end", }, ) - wire = result["wire"] - self.assertIn("collision_warnings", wire.QetRouteIssueCodes) - self.assertIn("third_party_device_collisions", wire.QetRouteIssueCodes) - self.assertIn("第三方设备/布局碰撞", wire.QetRouteIssueLabels) - payload = json.loads(wire.QetRouteDiagnosticsJson) - self.assertIn("third_party_device_collisions", payload["issue_codes"]) - self.assertEqual( - "third_party_device_collision", - payload["collisions"][0]["collision_relation"], - ) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) - def test_collision_relation_marks_endpoint_device_collision(self): + def test_network_route_limits_local_access_obstacles_to_nearby_bboxes(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - - relation = auto_routing._collision_relation( - { - "obstacle_element_uuid": "device-start", - "start_element_uuid": "device-start", - "end_element_uuid": "device-end", - } + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], + project_uuid="project-1", + kind="WireDuct", + ) + near_obstacle = doc.addObject("Part::Feature", "NearThirdPartyDevice") + near_obstacle.Label = "近处第三方设备" + terminal_objects.ensure_string_property( + near_obstacle, + "QetElementUuid", + "QET Exchange", + "", + "device-near-obstacle", ) + near_obstacle.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) + for index in range(120): + far_obstacle = doc.addObject("Part::Feature", "FarDevice{0}".format(index)) + far_obstacle.Shape = FakeShape( + FakeBoundBox(10000 + index * 20, 10010 + index * 20, 10000, 10010, 10000, 10010) + ) - self.assertEqual("endpoint_device_collision", relation) + calls = {"count": 0} + original_segment_intersects_bbox = auto_routing._segment_intersects_bbox - def test_unbound_structural_collision_can_be_auto_ignored_without_ignoring_devices(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + def counted_segment_intersects_bbox(start_point, end_point, bbox): + calls["count"] += 1 + return original_segment_intersects_bbox(start_point, end_point, bbox) - structural = { - "obstacle_label": "NFB BRACKET_P00", - "obstacle_name": "Solid043", - "obstacle_element_uuid": "", - "obstacle_parent_labels": ["CABINET ASS'Y", "QET Exchange Devices"], - "obstacle_parent_names": ["LinkGroup005", "QETExchangeDevices"], - } - device = { - "obstacle_label": "3S001", - "obstacle_name": "Device3S001", - "obstacle_element_uuid": "device-uuid", - "obstacle_parent_labels": ["QET Exchange Devices"], - "obstacle_parent_names": ["QETExchangeDevices"], - } + auto_routing._segment_intersects_bbox = counted_segment_intersects_bbox + try: + result = auto_routing.build_network_route( + start, + end, + options={ + "avoid_obstacles": False, + "avoid_local_access_obstacles": True, + "terminal_exit_length": 0.0, + }, + doc=doc, + ) + finally: + auto_routing._segment_intersects_bbox = original_segment_intersects_bbox - self.assertTrue(auto_routing._is_auto_ignorable_unbound_structural_collision(structural)) - self.assertFalse(auto_routing._is_auto_ignorable_unbound_structural_collision(device)) - kept, ignored = auto_routing._filter_auto_ignored_collisions([structural, device]) - self.assertEqual([device], kept) - self.assertEqual([structural], ignored) + self.assertIsNotNone(result) + self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) + self.assertLess(calls["count"], 80) - def test_eplan_connection_route_prefers_wire_duct_when_routing_range_is_only_moderately_shorter(self): + def test_network_route_ignores_unbound_structural_bboxes_for_local_access_avoidance(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(10, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], - project_uuid="project-1", - kind="RoutingRange", - ) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 100, 0)) routing_network.create_route_carrier( doc, - [ - app.Vector(0, 0, 20), - app.Vector(0, 145, 20), - app.Vector(10, 145, 20), - app.Vector(10, 0, 20), - ], + [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], project_uuid="project-1", kind="WireDuct", ) + near_device = doc.addObject("Part::Feature", "BoundNearDevice") + terminal_objects.ensure_string_property( + near_device, + "QetElementUuid", + "QET Exchange", + "", + "device-near-obstacle", + ) + near_device.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) + for index in range(80): + cabinet_part = doc.addObject("Part::Feature", "ImportedCabinetPart{0}".format(index)) + cabinet_part.Shape = FakeShape(FakeBoundBox(-1000, 1000, -1000, 1000, -1000, 1000)) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + calls = {"count": 0} + original_segment_intersects_bbox = auto_routing._segment_intersects_bbox - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + def counted_segment_intersects_bbox(start_point, end_point, bbox): + calls["count"] += 1 + return original_segment_intersects_bbox(start_point, end_point, bbox) - def test_eplan_connection_route_considers_primary_entry_beyond_nearest_surface_candidates(self): + auto_routing._segment_intersects_bbox = counted_segment_intersects_bbox + try: + result = auto_routing.build_network_route( + start, + end, + options={ + "avoid_obstacles": False, + "avoid_local_access_obstacles": True, + "terminal_exit_length": 0.0, + }, + doc=doc, + ) + finally: + auto_routing._segment_intersects_bbox = original_segment_intersects_bbox + + self.assertIsNotNone(result) + self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) + self.assertLess(calls["count"], 80) + + def test_network_route_caps_extra_entry_candidates_in_batch_mode(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -9368,954 +10058,3172 @@ class AutoRoutingTest(unittest.TestCase): terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - for y in range(1, 11): + for index in range(8): routing_network.create_route_carrier( doc, - [app.Vector(0, y, 20), app.Vector(100, y, 20)], + [app.Vector(0, index * 10, 50), app.Vector(100, index * 10, 50)], project_uuid="project-1", - kind="RoutingRange", + kind="WireDuct", ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], - project_uuid="project-1", - kind="WireDuct", + obstacle = doc.addObject("Part::Feature", "BoundNearDevice") + terminal_objects.ensure_string_property( + obstacle, + "QetElementUuid", + "QET Exchange", + "", + "device-near-obstacle", ) + obstacle.Shape = FakeShape(FakeBoundBox(15, 25, -5, 5, 40, 60)) + calls = {"shortest_path": 0} + original_shortest_path = routing_network.shortest_path_with_carriers - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + def counted_shortest_path(*args, **kwargs): + calls["shortest_path"] += 1 + return original_shortest_path(*args, **kwargs) - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + routing_network.shortest_path_with_carriers = counted_shortest_path + try: + result = auto_routing.build_network_route( + start, + end, + options={ + "network_entry_candidate_limit": 3, + "network_entry_candidate_total_limit": 4, + "avoid_obstacles": False, + "avoid_local_access_obstacles": True, + "terminal_exit_length": 0.0, + }, + doc=doc, + ) + finally: + routing_network.shortest_path_with_carriers = original_shortest_path - def test_route_eplan_connections_from_payload_skips_tasks_when_carriers_have_no_segments(self): - _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + self.assertIsNotNone(result) + self.assertLessEqual(calls["shortest_path"], 16) + + def test_eplan_connection_route_marks_clearance_warning_against_expanded_obstacle_bbox(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - broken_carrier = doc.addObject("Part::Feature", "BrokenCarrier") - terminal_objects.ensure_string_property( - broken_carrier, - "QetRoutingRole", - "QET Routing", - "Routing role marker", - "RoutingCarrier", - ) - terminal_objects.ensure_string_property( - broken_carrier, - "QetRouteCarrierKind", - "QET Routing", - "Route carrier kind", - "WireDuct", - ) - terminal_objects.ensure_bool_property( - broken_carrier, - "CanRouteWire", - "QET Routing", - "Whether routing connections can use this path", - True, + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], + label="主线槽A", + project_uuid="project-1", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - }, - ], - } + obstacle = doc.addObject("Part::Feature", "NearObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 3, 6, 90, 110)) - report = auto_routing.route_eplan_connections_from_payload( + result = auto_routing.route_eplan_connection_between_terminals( doc, - payload, - options={"network_entry_max_distance": 30.0}, + start, + end, + options={"obstacle_clearance": 5.0}, ) - self.assertEqual(1, report["route_network_carriers"]) - self.assertEqual(0, report["route_network_segments"]) - self.assertEqual(0, report["route_network_nodes"]) - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) - self.assertEqual([], report["errors"]) - self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + self.assertEqual("CollisionWarning", result["route_status"]) + self.assertEqual(1, result["collision_count"]) + self.assertEqual("ClearanceWarning", result["collisions"][0]["collision_kind"]) + self.assertEqual(3.0, result["collisions"][0]["obstacle_bbox"]["ymin"]) + self.assertEqual(-2.0, result["collisions"][0]["collision_bbox"]["ymin"]) + self.assertEqual("1", result["wire"].QetRouteCollisionCount) + self.assertEqual("0", result["wire"].QetRouteHardIntersectionCount) + self.assertEqual("1", result["wire"].QetRouteClearanceWarningCount) + self.assertEqual("ClearanceWarning", result["wire"].QetRouteCollisionStatus) - def test_route_eplan_connections_from_payload_applies_batch_entry_candidate_limit(self): + def test_eplan_connection_route_ignores_terminal_exit_segment_collision(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - kind="WireDuct", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - }, - ], - } - captured_options = [] - original = auto_routing.route_eplan_connection_between_terminals - - def fake_route(*args, **kwargs): - captured_options.append(dict(kwargs.get("options") or {})) - return { - "algorithm": "fake", - "route_status": "Routed", - "length_mm": 10.0, - "lane": {"index": 0}, - "network": {}, - "route_track": {}, - "collision_count": 0, - "collisions": [], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a", - } + terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") + terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) - auto_routing.route_eplan_connection_between_terminals = fake_route - try: - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={ - "network_entry_candidate_limit": 8, - "batch_network_entry_candidate_limit": 2, - "batch_network_entry_total_candidate_limit": 4, - }, - ) - finally: - auto_routing.route_eplan_connection_between_terminals = original + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual(1, report["routed"]) - self.assertEqual(2, report["batch_network_entry_candidate_limit"]) - self.assertEqual(4, report["batch_network_entry_total_candidate_limit"]) - self.assertFalse(report["batch_avoid_obstacles"]) - self.assertEqual(2, captured_options[0]["network_entry_candidate_limit"]) - self.assertEqual(4, captured_options[0]["network_entry_candidate_total_limit"]) - self.assertFalse(captured_options[0]["avoid_obstacles"]) - self.assertIsInstance(captured_options[0]["__base_route_network"], dict) - self.assertIsInstance(captured_options[0]["__obstacle_candidate_cache"], dict) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) - def test_route_eplan_connections_retries_missing_route_with_wider_candidate_limit(self): + def test_eplan_connection_route_ignores_explicit_start_local_route_collision(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + start.QetTerminalLocalRoutePointsJson = json.dumps( + [[0, 0, 0], [20, 0, 0], [20, 40, 0]] + ) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(20, 80, 0), app.Vector(120, 80, 0)], + label="Cabinet Main Path", project_uuid="project-1", - kind="WireDuct", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - }, - ], - } - captured_limits = [] - original = auto_routing.route_eplan_connection_between_terminals - - def fake_route(*args, **kwargs): - limit = int((kwargs.get("options") or {}).get("network_entry_candidate_limit", 0) or 0) - captured_limits.append(limit) - if limit < 8: - raise auto_routing.AutoRoutingError( - "没有可用的布线路径网络;请先生成布线布局空间和布线路径网络。" - ) - return { - "algorithm": "fake", - "route_status": "Routed", - "length_mm": 10.0, - "lane": {"index": 0}, - "network": {}, - "route_track": {}, - "collision_count": 0, - "collisions": [], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a", - } + local_body = doc.addObject("Part::Feature", "StartDeviceLocalShell") + local_body.Shape = FakeShape(FakeBoundBox(15, 25, 15, 25, -5, 5)) - auto_routing.route_eplan_connection_between_terminals = fake_route - try: - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={ - "network_entry_candidate_limit": 8, - "batch_network_entry_candidate_limit": 3, - "missing_route_retry_candidate_limit": 8, - }, - ) - finally: - auto_routing.route_eplan_connection_between_terminals = original + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + ) - self.assertEqual([3, 8], captured_limits) - self.assertEqual(1, report["routed"]) - self.assertEqual(0, report["skipped_missing_route_network"]) - self.assertEqual(1, report["missing_route_retries"]) - self.assertEqual(1, report["route_status_counts"]["Routed"]) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) + self.assertEqual(3, len(diagnostics["endpoint_access"]["start_points"])) - def test_route_eplan_connections_selectively_reroutes_third_party_collisions(self): + def test_eplan_connection_route_still_reports_main_path_collision_after_local_route(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + start.QetTerminalLocalRoutePointsJson = json.dumps( + [[0, 0, 0], [20, 0, 0], [20, 40, 0]] + ) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(20, 80, 0), app.Vector(120, 80, 0)], + label="Cabinet Main Path", project_uuid="project-1", - kind="WireDuct", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_element_uuid": "device-start", - "start_terminal_uuid": "terminal-start", - "end_element_uuid": "device-end", - "end_terminal_uuid": "terminal-end", - }, - ], - } - captured_avoid = [] - original = auto_routing.route_eplan_connection_between_terminals + main_obstacle = doc.addObject("Part::Feature", "MainPathObstacle") + main_obstacle.Shape = FakeShape(FakeBoundBox(55, 65, 75, 85, -5, 5)) - def fake_route(*args, **kwargs): - route_options = dict(kwargs.get("options") or {}) - avoid = bool(route_options.get("avoid_obstacles", False)) - captured_avoid.append(avoid) - if avoid: - return { - "algorithm": "fake", - "route_status": "Routed", - "length_mm": 12.0, - "lane": {"index": 0}, - "network": {}, - "route_track": {}, - "collision_count": 0, - "collisions": [], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a clean", - } - return { - "algorithm": "fake", - "route_status": "CollisionWarning", - "length_mm": 10.0, - "lane": {"index": 0}, - "network": {}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} - ] - }, - "collision_count": 1, - "collisions": [ - { - "collision_kind": "HardIntersection", - "obstacle_element_uuid": "device-obstacle", - "obstacle_label": "设备A", - } - ], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a collision", - } - - auto_routing.route_eplan_connection_between_terminals = fake_route - try: - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - finally: - auto_routing.route_eplan_connection_between_terminals = original + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + ) - self.assertEqual([False, True], captured_avoid) - self.assertEqual(1, report["selective_collision_reroute_attempts"]) - self.assertEqual(1, report["selective_collision_reroutes"]) - self.assertEqual(0, report["selective_collision_reroute_no_improvement"]) - self.assertEqual(1, report["routed"]) - self.assertEqual(0, report["collision_warnings"]) - self.assertEqual("Routed", report["routes"][0]["route_status"]) + self.assertEqual("CollisionWarning", result["route_status"]) + self.assertEqual(1, result["collision_count"]) + self.assertEqual("MainPathObstacle", result["collisions"][0]["obstacle_name"]) - def test_route_eplan_connections_rejects_selective_reroute_when_it_uses_fallback_path(self): + def test_eplan_connection_route_detours_local_access_segment_around_obstacle(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], + label="主线槽A", project_uuid="project-1", - kind="WireDuct", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_element_uuid": "device-start", - "start_terminal_uuid": "terminal-start", - "end_element_uuid": "device-end", - "end_terminal_uuid": "terminal-end", - }, - ], - } - original = auto_routing.route_eplan_connection_between_terminals - created_wires = [] + obstacle = doc.addObject("Part::Feature", "AccessObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, 40, 60)) - def fake_route(*args, **kwargs): - route_doc = args[0] - avoid = bool((kwargs.get("options") or {}).get("avoid_obstacles", False)) - if avoid: - retry_wire = route_doc.addObject("Part::Feature", "RetryFallbackWire") - created_wires.append(retry_wire) - return { - "wire": retry_wire, - "algorithm": "fake", - "route_status": "Routed", - "length_mm": 12.0, - "lane": {"index": 0}, - "network": {}, - "route_track": { - "segments": [ - {"carrier": {"kind": "RoutingRange", "label": "辅助面"}} - ] - }, - "collision_count": 0, - "collisions": [], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a fallback", - } - original_wire = route_doc.addObject("Part::Feature", "OriginalCollisionWire") - created_wires.append(original_wire) - return { - "wire": original_wire, - "algorithm": "fake", - "route_status": "CollisionWarning", - "length_mm": 10.0, - "lane": {"index": 0}, - "network": {}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} - ] - }, - "collision_count": 1, - "collisions": [ - { - "collision_kind": "HardIntersection", - "obstacle_element_uuid": "device-obstacle", - "obstacle_label": "设备A", - } - ], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a collision", - } + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - auto_routing.route_eplan_connection_between_terminals = fake_route - try: - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - finally: - auto_routing.route_eplan_connection_between_terminals = original + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + self.assertTrue( + any(abs(point.x) > 5.0 or abs(point.y) > 5.0 for point in result["points"]), + "局部接入段应增加侧向绕障拐点,而不是直接穿过障碍盒。", + ) - self.assertEqual(1, report["selective_collision_reroute_attempts"]) - self.assertEqual(0, report["selective_collision_reroutes"]) - self.assertEqual(1, report["selective_collision_reroute_rejected_fallback"]) - self.assertEqual(1, report["collision_warnings"]) - self.assertEqual("CollisionWarning", report["routes"][0]["route_status"]) - self.assertEqual("RejectedFallback", report["routes"][0]["selective_collision_reroute_status"]) - self.assertEqual( - ["RoutingRange"], - report["routes"][0]["selective_collision_reroute_rejected_fallback_kinds"], + def test_terminal_access_corrects_default_lcs_direction_when_bbox_exit_is_too_deep(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceDeepBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalDeepInside", "terminal-deep", app.Vector(0, 0, 0)) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "DeepDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 500)) + device.addObject(body) + + points = routing_network.terminal_access_path_points( + terminal, + exit_length=20.0, + max_exit_length=80.0, ) - self.assertEqual( - ["辅助面"], - report["routes"][0]["selective_collision_reroute_rejected_fallback_labels"], + diagnostics = routing_network.terminal_access_diagnostics( + terminal, + exit_length=20.0, + max_exit_length=80.0, ) - self.assertIn("main_path_detour_missing", report["routes"][0]["issue_codes"]) - compact = auto_routing._compact_routing_connection_batch_report(report) - self.assertIn("main_path_detour_missing", compact["route_samples"][0]["issue_codes"]) - self.assertEqual( - ["辅助面"], - compact["route_samples"][0]["selective_collision_reroute"]["rejected_fallback_labels"], + + self.assertEqual(2, len(points)) + self.assertEqual(20.0, points[-1].x) + self.assertEqual(0.0, points[-1].z) + self.assertTrue(diagnostics["exit_direction_corrected"]) + self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, diagnostics["exit_direction"]) + + def test_terminal_access_exit_length_is_capped_for_explicit_deep_direction(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceDeepBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalDeepInside", "terminal-deep", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + terminal.QetTerminalExitDirectionJson = json.dumps({"x": 0, "y": 0, "z": 1}) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "DeepDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 500)) + device.addObject(body) + + points = routing_network.terminal_access_path_points( + terminal, + exit_length=20.0, + max_exit_length=80.0, ) - self.assertEqual(1, report["main_path_detour_missing_summary"]["wire_count"]) - self.assertEqual( - {"辅助面": 1}, - report["main_path_detour_missing_summary"]["rejected_fallback_label_counts"], + diagnostics = routing_network.terminal_access_diagnostics( + terminal, + exit_length=20.0, + max_exit_length=80.0, ) - self.assertEqual( - {"主线槽A": 1}, - report["main_path_detour_missing_summary"]["current_route_source_label_counts"], + + self.assertEqual(80.0, points[-1].z) + self.assertFalse(diagnostics["exit_direction_corrected"]) + self.assertEqual("explicit", diagnostics["exit_direction_source"]) + + def test_terminal_access_diagnostics_reports_capped_device_exit(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceDeepBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalDeepInside", "terminal-deep", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + terminal.QetTerminalExitDirectionJson = json.dumps({"x": 0, "y": 0, "z": 1}) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "DeepDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 500)) + device.addObject(body) + + payload = routing_network.terminal_access_diagnostics( + terminal, + exit_length=20.0, + max_exit_length=80.0, ) - self.assertEqual( - {"辅助面 -> 主线槽A": 1}, - report["main_path_detour_missing_summary"]["bridge_pair_counts"], + + self.assertTrue(payload["exit_length_capped"]) + self.assertEqual(20.0, payload["requested_exit_length_mm"]) + self.assertEqual(80.0, payload["actual_exit_length_mm"]) + self.assertEqual(510.0, payload["device_exit_required_length_mm"]) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, payload["exit_direction"]) + + def test_terminal_access_uses_explicit_terminal_exit_direction_before_lcs_direction(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + terminal = _terminal(doc, terminal_objects, "TerminalWithExplicitDirection", "terminal-dir", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + terminal.QetTerminalExitDirectionJson = json.dumps({"x": 1, "y": 0, "z": 0}) + + points = routing_network.terminal_access_path_points(terminal, exit_length=30.0) + diagnostics = routing_network.terminal_access_diagnostics(terminal, exit_length=30.0) + + self.assertEqual(30.0, points[-1].x) + self.assertEqual(0.0, points[-1].z) + self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, diagnostics["exit_direction"]) + + def test_terminal_access_local_route_overrides_explicit_direction_and_reports_source(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + terminal = _terminal(doc, terminal_objects, "TerminalWithLocalRoute", "terminal-local", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + terminal.QetTerminalExitDirectionJson = json.dumps({"x": 0, "y": 0, "z": 1}) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps( + [[0, 0, 0], [15, 0, 0], [15, 35, 0]] ) - self.assertEqual( - ["点击“选择缺主路径补路位置”快速定位汇总需补区域"], - [ - action - for action in report["recommended_actions"] - if "选择缺主路径补路位置" in action - ], + + points = routing_network.terminal_access_path_points( + terminal, + exit_length=30.0, + max_exit_length=40.0, ) - self.assertIn("main_path_detour_missing", created_wires[0].QetRouteIssueCodes) - wire_payload = json.loads(created_wires[0].QetRouteDiagnosticsJson) - self.assertEqual( - ["辅助面"], - wire_payload["selective_collision_reroute"]["rejected_fallback_labels"], + diagnostics = routing_network.terminal_access_diagnostics( + terminal, + exit_length=30.0, + max_exit_length=40.0, ) - self.assertIn("main_path_detour_missing", report["issue_codes"]) - message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("局部避障:尝试 1 条,接受 0 条,拒绝辅助路径 1 条", message) - self.assertIn("请补主路径/UserPath 或调整装配", message) - self.assertIn("缺主路径绕行:1 条,需补路径位置:辅助面 1 条", message) - self.assertIn("辅助面 -> 主线槽A 1 条", message) - def test_route_eplan_connections_auto_bridges_main_path_detour_pairs_and_retries_once(self): + self.assertEqual([(0.0, 0.0, 0.0), (15.0, 0.0, 0.0), (15.0, 35.0, 0.0)], [(p.x, p.y, p.z) for p in points]) + self.assertTrue(diagnostics["local_route_used"]) + self.assertEqual("local_route", diagnostics["exit_rule"]) + self.assertEqual(3, diagnostics["local_route_point_count"]) + self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, diagnostics["exit_direction"]) + self.assertEqual({"x": 15.0, "y": 35.0, "z": 0.0}, diagnostics["exit_point"]) + + def test_terminal_access_prefers_main_path_over_nearer_routing_range_and_records_rule(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") - fallback_source.Label = "门板布线面" - current_source = doc.addObject("Part::Feature", "MainDuctSource") - current_source.Label = "主线槽A" - fallback_carrier = routing_network.create_route_carrier( + terminal = _terminal(doc, terminal_objects, "TerminalNearPanel", "terminal-panel", app.Vector(0, 0, 0)) + routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(8, 0, 20), app.Vector(8, 80, 20)], + label="安装板布线面", project_uuid="project-1", kind="RoutingRange", - label="门板布线面 carrier", ) - current_carrier = routing_network.create_route_carrier( + main_path = routing_network.create_route_carrier( doc, - [app.Vector(140, 20, 20), app.Vector(240, 20, 20)], + [app.Vector(40, 0, 20), app.Vector(40, 80, 20)], + label="柜内主路径", project_uuid="project-1", - kind="WireDuct", - label="主线槽A carrier", + kind="UserPath", ) - fallback_carrier.QetRouteSourceName = fallback_source.Name - fallback_carrier.QetRouteSourceLabel = fallback_source.Label - current_carrier.QetRouteSourceName = current_source.Name - current_carrier.QetRouteSourceLabel = current_source.Label - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_element_uuid": "device-start", - "start_terminal_uuid": "terminal-start", - "end_element_uuid": "device-end", - "end_terminal_uuid": "terminal-end", - }, - ], - } - original = auto_routing.route_eplan_connection_between_terminals - calls = [] - def fake_route(*args, **kwargs): - route_doc = args[0] - calls.append(bool((kwargs.get("options") or {}).get("avoid_obstacles", False))) - detour_path_exists = any( - getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourPath" - for carrier in routing_network.collect_route_carriers(route_doc) - ) - wire = route_doc.addObject("Part::Feature", "WireAfterDetourPath" if detour_path_exists else "WireBeforeDetourPath") - if detour_path_exists: - return { - "wire": wire, - "algorithm": "fake", - "route_status": "Routed", - "length_mm": 10.0, - "lane": {"index": 0}, - "network": {}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} - ] - }, - "collision_count": 0, - "collisions": [], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a routed", - } - if bool((kwargs.get("options") or {}).get("avoid_obstacles", False)): - points = [ - app.Vector(0, 0, 0), - app.Vector(0, 0, 20), - app.Vector(80, 0, 20), - app.Vector(140, 20, 20), - app.Vector(100, 0, 0), - ] - wire.Points = points - return { - "wire": wire, - "algorithm": "fake", - "route_status": "Routed", - "length_mm": 12.0, - "lane": {"index": 0}, - "network": {}, - "points": points, - "route_track": { - "segments": [ - {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} - ] - }, - "collision_count": 0, - "collisions": [], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a fallback", - } - return { - "wire": wire, - "algorithm": "fake", - "route_status": "CollisionWarning", - "length_mm": 10.0, - "lane": {"index": 0}, - "network": {}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} - ] - }, - "collision_count": 1, - "collisions": [ - { - "collision_kind": "HardIntersection", - "obstacle_element_uuid": "device-obstacle", - "obstacle_label": "设备A", - } - ], - "wire_style_status": "NotRequested", - "wire_object_label": "wire-a collision", - } + carriers = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + ) - auto_routing.route_eplan_connection_between_terminals = fake_route - try: - report = auto_routing.route_eplan_connections( - doc, - payload=payload, - options={"auto_create_main_path_detour_bridges": True}, - project_uuid="project-1", - update_network=False, - ) - finally: - auto_routing.route_eplan_connection_between_terminals = original + self.assertEqual(1, len(carriers)) + access = carriers[0] + self.assertEqual("UserPath", access.QetTerminalAccessTargetKind) + self.assertEqual(main_path.Name, access.QetTerminalAccessTargetName) + self.assertEqual("main_path_preferred_over_fallback", access.QetTerminalAccessTargetRule) + self.assertEqual("0", access.QetTerminalAccessFallbackTarget) - bridges = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourBridge" - ] - detour_paths = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourPath" - ] + def test_terminal_access_from_local_route_avoids_reentering_endpoint_device_bbox(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceAccessBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalWithLocalExit", "terminal-local-exit", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [20, 0, 0]]) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "EndpointDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 10)) + device.addObject(body) + routing_network.create_route_carrier( + doc, + [app.Vector(-20, 0, 0), app.Vector(-20, 80, 0)], + label="左侧主路径", + project_uuid="project-1", + kind="UserPath", + ) - self.assertEqual(1, report["routed"]) - self.assertEqual(0, report["collision_warnings"]) - self.assertEqual({"Routed": 1}, report["route_status_counts"]) - self.assertEqual(1, report["auto_main_path_detour_bridges"]["created_count"]) - self.assertTrue(report["auto_main_path_detour_bridges"]["rerouted"]) - self.assertEqual(1, report["auto_main_path_detour_bridges"]["retry_wires"]) - self.assertEqual(1, report["auto_main_path_detour_bridges"]["retry_replaced_routes"]) - self.assertEqual("门板布线面 -> 主线槽A", bridges[0].QetRouteBridgePairLabel) - self.assertEqual("门板布线面 -> 主线槽A", detour_paths[0].QetRouteBridgePairLabel) - self.assertEqual([False, True, False], calls) - compact = auto_routing._compact_routing_connection_batch_report(report) - message = auto_routing.format_eplan_connection_route_report(report) - self.assertEqual(1, compact["auto_main_path_detour_bridges"]["created_count"]) - self.assertIn("自动主路径补桥:生成 UserPath 1 条并重跑布线", message) + carriers = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + ) - def test_auto_main_path_detour_user_path_raises_capacity_when_same_path_reused(self): + self.assertEqual(1, len(carriers)) + access = carriers[0] + self.assertEqual("1", access.QetTerminalAccessAvoidedEndpointDevice) + points = [(round(point.x, 3), round(point.y, 3), round(point.z, 3)) for point in access.Points] + self.assertNotEqual([(0.0, 0.0, 0.0), (20.0, 0.0, 0.0), (-20.0, 0.0, 0.0)], points) + for start, end in zip(access.Points[1:], access.Points[2:]): + self.assertFalse( + routing_network._segment_intersects_bbox_payload( + start, + end, + {"xmin": -10, "xmax": 10, "ymin": -10, "ymax": 10, "zmin": -10, "zmax": 10}, + ), + "局部出线后的 TerminalAccess 接入段不应重新穿过端点设备包围盒。", + ) + + def test_routing_path_diagnostic_reports_terminal_access_fallback_targets(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - points = [ - app.Vector(0, 0, 0), - app.Vector(0, 0, 20), - app.Vector(100, 0, 20), - app.Vector(100, 0, 0), - ] - retry_result = { - "points": points, - "route_track": { - "segments": [ - {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} - ] - }, - } - original_result = { - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} - ] - }, - } - - first = auto_routing._create_main_path_detour_user_path_from_retry( + _terminal(doc, terminal_objects, "TerminalFallbackAccess", "terminal-fallback", app.Vector(0, 0, 0)) + fallback = routing_network.create_route_carrier( doc, - retry_result, - original_result, + [app.Vector(20, 0, 20), app.Vector(20, 80, 20)], + label="安装板兜底路径", project_uuid="project-1", + kind="RoutingRange", ) - second = auto_routing._create_main_path_detour_user_path_from_retry( + routing_network.create_terminal_access_carriers_from_document( doc, - retry_result, - original_result, project_uuid="project-1", + terminal_exit_length=20.0, ) - self.assertIs(first, second) - self.assertEqual(2, first.QetRouteCarrierCapacity) + diagnostic = routing_network.diagnose_routing_path_network(doc) - def test_auto_main_path_detour_user_path_initial_capacity_matches_lane_parallel_count(self): + self.assertIn("terminal_access_fallback_targets", diagnostic["issue_codes"]) + self.assertEqual(1, len(diagnostic["terminal_access_fallback_targets"])) + sample = diagnostic["terminal_access_fallback_targets"][0] + self.assertEqual("TerminalFallbackAccess", sample["terminal_name"]) + self.assertEqual(fallback.Name, sample["target_name"]) + self.assertEqual("RoutingRange", sample["target_kind"]) + self.assertEqual(20.0, sample["access_length_mm"]) + self.assertEqual( + [{"x": 0.0, "y": 0.0, "z": 20.0}, {"x": 20.0, "y": 0.0, "z": 20.0}], + sample["access_points"], + ) + + def test_routing_path_diagnostic_reports_terminal_access_endpoint_device_avoidance(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - retry_result = { - "points": [ - app.Vector(0, 0, 0), - app.Vector(0, 0, 20), - app.Vector(100, 0, 20), - app.Vector(100, 0, 0), - ], - "lane": {"index": 1}, - "route_track": { - "segments": [ - {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} - ] - }, - } - original_result = { - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} - ] - }, - } + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceAccessBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalWithLocalExit", "terminal-local-exit", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [20, 0, 0]]) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "EndpointDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 10)) + device.addObject(body) + routing_network.create_route_carrier( + doc, + [app.Vector(-20, 0, 0), app.Vector(-20, 80, 0)], + label="左侧主路径", + project_uuid="project-1", + kind="UserPath", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + ) - carrier = auto_routing._create_main_path_detour_user_path_from_retry( + diagnostic = routing_network.diagnose_routing_path_network(doc) + + self.assertIn("terminal_access_endpoint_device_avoidance", diagnostic["issue_codes"]) + self.assertEqual(1, len(diagnostic["terminal_access_endpoint_device_avoidance"])) + sample = diagnostic["terminal_access_endpoint_device_avoidance"][0] + self.assertEqual("TerminalWithLocalExit", sample["terminal_name"]) + self.assertEqual("QETDeviceAccessBox", sample["parent_device_name"]) + self.assertEqual("UserPath", sample["target_kind"]) + self.assertGreater(sample["access_length_mm"], 0.0) + self.assertGreaterEqual(len(sample["access_points"]), 2) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, sample["access_points"][0]) + + def test_routing_path_diagnostic_reports_terminal_exit_direction_corrections(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceDeepBox") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalDeepInside", "terminal-deep", app.Vector(0, 0, 0)) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "DeepDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 500)) + device.addObject(body) + routing_network.create_route_carrier( doc, - retry_result, - original_result, + [app.Vector(20, 0, 0), app.Vector(20, 80, 0)], + label="侧向主路径", project_uuid="project-1", + kind="UserPath", ) - self.assertEqual(2, carrier.QetRouteCarrierCapacity) + diagnostic = routing_network.diagnose_routing_path_network( + doc, + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + ) - def test_route_report_raises_auto_detour_path_capacity_from_final_lane_usage(self): + self.assertIn("terminal_exit_direction_corrected", diagnostic["issue_codes"]) + self.assertEqual(1, len(diagnostic["corrected_terminal_exits"])) + sample = diagnostic["corrected_terminal_exits"][0] + self.assertEqual("TerminalDeepInside", sample["name"]) + self.assertEqual("terminal-deep", sample["terminal_uuid"]) + self.assertTrue(sample["exit_direction_corrected"]) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, sample["original_exit_direction"]) + self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, sample["exit_direction"]) + self.assertEqual(510.0, sample["original_device_exit_required_length_mm"]) + + def test_routed_wire_diagnostics_include_terminal_access_endpoint_device_avoidance(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - carrier = routing_network.create_route_carrier( + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceAccessBox") + root.addObject(device) + start = _terminal(doc, terminal_objects, "TerminalWithLocalExit", "terminal-local-exit", app.Vector(0, 0, 0)) + start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + start.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [20, 0, 0]]) + device.addObject(start) + body = doc.addObject("Part::Feature", "EndpointDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 10)) + device.addObject(body) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(-20, 80, 0)) + routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(-20, 0, 0), app.Vector(-20, 80, 0)], + label="左侧主路径", project_uuid="project-1", kind="UserPath", - capacity=1, ) - carrier.QetRouteBridgeKind = "MainPathDetourPath" - report = { - "routes": [ - { - "wire_uuid": "wire-auto-detour", - "route_status": "Routed", - "lane": {"index": 1}, - "route_track": { - "segments": [ - { - "carrier": { - "name": carrier.Name, - "kind": "UserPath", - "capacity": 1, - } - } - ] - }, - } - ], - "skipped_missing_terminal": 0, - "skipped_missing_route_network": 0, - "skipped_invalid": 0, - "errors": [], - } + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + ) - auto_routing._raise_main_path_detour_capacities_from_report(doc, report) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 20.0}, + ) - self.assertEqual(2, carrier.QetRouteCarrierCapacity) - self.assertEqual(2, report["routes"][0]["route_track"]["segments"][0]["carrier"]["capacity"]) - self.assertEqual([], auto_routing._route_capacity_pressure_samples(report, limit=0)) + self.assertEqual("Routed", result["route_status"]) + diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) + self.assertTrue(diagnostics["network"]["start_terminal_access_avoided_endpoint_device"]) + self.assertFalse(diagnostics["network"]["end_terminal_access_avoided_endpoint_device"]) - def test_collect_obstacles_cache_preserves_endpoint_filters(self): + def test_routed_wire_diagnostics_include_terminal_exit_length_cap(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - - endpoint_body = doc.addObject("Part::Feature", "EndpointBody") - endpoint_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 5)) - endpoint_body.QetInstanceId = terminal.QetInstanceId - - near_body = doc.addObject("Part::Feature", "NearBody") - near_body.Shape = FakeShape(FakeBoundBox(1, 2, -1, 1, -1, 1)) - - far_body = doc.addObject("Part::Feature", "FarBody") - far_body.Shape = FakeShape(FakeBoundBox(80, 90, -1, 1, -1, 1)) - - options = {"terminal_exit_length": 20.0, "obstacle_clearance": 0.0} - uncached = auto_routing.collect_obstacles(doc, exclude=[terminal], options=options) - cache = auto_routing._obstacle_candidate_cache(doc, options=options) - cached = auto_routing.collect_obstacles( + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceDeepBox") + root.addObject(device) + start = _terminal(doc, terminal_objects, "TerminalDeepInside", "terminal-deep", app.Vector(0, 0, 0)) + start.addProperty("App::PropertyString", "QetTerminalExitDirectionJson", "QET Routing", "") + start.QetTerminalExitDirectionJson = json.dumps({"x": 0, "y": 0, "z": 1}) + device.addObject(start) + body = doc.addObject("Part::Feature", "DeepDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 500)) + device.addObject(body) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( doc, - exclude=[terminal], - options=dict(options, __obstacle_candidate_cache=cache), + [app.Vector(0, 0, 80), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="UserPath", ) - self.assertEqual(["FarBody"], [item["name"] for item in uncached]) - self.assertEqual(["FarBody"], [item["name"] for item in cached]) - - def test_collect_obstacles_skips_parent_of_support_surface_source(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - doc = FakeDocument() - parent = doc.addObject("App::LinkGroup", "DoorAssembly") - parent.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) - panel = doc.addObject("Part::Feature", "DoorPanel") - panel.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 2)) - panel.QetRoutingObstacleMode = "SupportSurface" - parent.addObject(panel) - obstacle = doc.addObject("Part::Feature", "DeviceObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) - - obstacles = auto_routing.collect_obstacles(doc) - cache = auto_routing._obstacle_candidate_cache(doc) - cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) - - self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) - self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) - - def test_collect_obstacles_skips_descendant_of_pass_through_ancestor(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - doc = FakeDocument() - assembly = doc.addObject("App::LinkGroup", "DoorAssembly") - assembly.QetRoutingObstacleMode = "PassThrough" - compound = doc.addObject("Part::Compound2", "DoorCompound") - panel = doc.addObject("Part::Feature", "DoorPanel") - panel.Shape = FakeShape(FakeBoundBox(0, 40, 0, 40, 0, 40)) - assembly.addObject(compound) - compound.addObject(panel) - obstacle = doc.addObject("Part::Feature", "DeviceObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) - - obstacles = auto_routing.collect_obstacles(doc) - cache = auto_routing._obstacle_candidate_cache(doc) - cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) - - self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) - self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) - - def test_collect_obstacles_reports_full_parent_chain_for_nested_import_parts(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - doc = FakeDocument() - assembly = doc.addObject("App::LinkGroup", "DoorAssembly") - assembly.Label = "FRONT DOOR-R ASS'Y" - compound = doc.addObject("Part::Compound2", "DoorCompound") - compound.Label = "NAUO141" - panel = doc.addObject("Part::Feature", "DoorPanel") - panel.Shape = FakeShape(FakeBoundBox(0, 40, 0, 40, 0, 40)) - assembly.addObject(compound) - compound.addObject(panel) - - obstacles = auto_routing.collect_obstacles(doc) - - self.assertEqual(["DoorPanel"], [item["name"] for item in obstacles]) - self.assertEqual(["DoorCompound", "DoorAssembly"], obstacles[0]["parent_refs"]["names"]) - self.assertEqual(["NAUO141", "FRONT DOOR-R ASS'Y"], obstacles[0]["parent_refs"]["labels"]) - - def test_collect_obstacles_skips_auto_detected_support_surface_candidate(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - doc = FakeDocument() - side_cover = doc.addObject("Part::Feature", "SideCover") - side_cover.Label = "SIDE COVER-1_P00" - side_cover.Shape = FakeShape(FakeBoundBox(0, 600, 0, 2148, 0, 30)) - obstacle = doc.addObject("Part::Feature", "DeviceObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) - - obstacles = auto_routing.collect_obstacles(doc) - cache = auto_routing._obstacle_candidate_cache(doc) - cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 20.0, "terminal_exit_max_length": 80.0}, + ) - self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) - self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + self.assertEqual("Routed", result["route_status"]) + diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) + start_diagnostics = diagnostics["endpoint_access"]["start_diagnostics"] + self.assertTrue(start_diagnostics["exit_length_capped"]) + self.assertEqual(80.0, start_diagnostics["actual_exit_length_mm"]) + self.assertEqual(510.0, start_diagnostics["device_exit_required_length_mm"]) - def test_collect_obstacles_skips_outlist_ancestor_of_support_surface_source(self): + def test_eplan_connection_route_ignores_endpoint_device_body_as_obstacle(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] doc = FakeDocument() - parent = doc.addObject("App::LinkGroup", "DoorAssembly") - parent.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) - compound = doc.addObject("Part::Compound2", "DoorCompound") - compound.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) - panel = doc.addObject("Part::Feature", "DoorPanel") - panel.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 2)) - panel.QetRoutingObstacleMode = "SupportSurface" - parent.OutList = [compound] - compound.InList = [parent] - compound.OutList = [panel] - panel.InList = [compound] - obstacle = doc.addObject("Part::Feature", "DeviceObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceStart") + device.QetInstanceId = start.QetInstanceId + device.addObject(start) + body = doc.addObject("Part::Feature", "StartDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) + device.addObject(body) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + ) - obstacles = auto_routing.collect_obstacles(doc) - cache = auto_routing._obstacle_candidate_cache(doc) - cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) - self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) - def test_route_eplan_connections_classifies_disconnected_network_as_missing_route_network(self): + def test_route_eplan_connections_from_payload_skips_missing_terminal(self): _install_fake_freecad() - terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(1000, 0, 20), app.Vector(1010, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) payload = { - "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-a", - "wire_label": "N4111", + "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", - "start_element_uuid": "QF1", - "start_terminal_display": "A1", - "end_terminal_uuid": "terminal-end", - "end_element_uuid": "KM1", - "end_terminal_display": "13", - }, - ], + "end_terminal_uuid": "terminal-missing", + "end_element_uuid": "device-missing", + "end_instance_id": "instance-missing", + "end_terminal_display": "A1", + } + ] } - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"network_entry_max_distance": 30.0}, - ) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) - self.assertEqual([], report["errors"]) - self.assertEqual("wire-a", report["missing_route_network_samples"][0]["wire_uuid"]) - self.assertEqual("N4111", report["missing_route_network_samples"][0]["wire_object_label"]) - self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + self.assertEqual(1, report["skipped_missing_terminal"]) + self.assertEqual(1, report["available_terminals"]) + self.assertEqual(0, report["local_terminals"]) + self.assertEqual(["terminal-missing"], report["missing_endpoint_uuids"]) + self.assertEqual("terminal-start", report["missing_endpoint_samples"][0]["start_terminal_uuid"]) + self.assertTrue(report["missing_endpoint_samples"][0]["start_found"]) + self.assertFalse(report["missing_endpoint_samples"][0]["end_found"]) + self.assertEqual("instance-missing", report["missing_endpoint_samples"][0]["end_instance_id"]) + self.assertEqual("A1", report["missing_endpoint_samples"][0]["end_terminal_display"]) + self.assertEqual(0, report["missing_endpoint_samples"][0]["end_element_terminal_count"]) + self.assertEqual([], report["missing_endpoint_samples"][0]["end_element_terminal_samples"]) + self.assertEqual(0, report["missing_endpoint_samples"][0]["end_instance_terminal_count"]) + self.assertEqual([], report["missing_endpoint_samples"][0]["end_instance_terminal_samples"]) + self.assertEqual( + "device_not_in_3d_scene", + report["missing_endpoint_samples"][0]["end_missing_endpoint_reason_code"], + ) + self.assertIn("终点 element=device-missing, instance=instance-missing, terminal=A1", message) + self.assertIn("原因=该 2D 设备未在 FreeCAD 场景中找到", message) + + def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_payload_devices(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + payload = { + "devices": [ + { + "element_uuid": "device-missing", + "instance_id": "instance-from-device-list", + "display_tag": "UD:8", + "terminals": [ + { + "terminal_uuid": "device-missing:terminal-a", + "terminal_display": "A1", + } + ], + } + ], + "wires": [ + { + "wire_id": "wire-1", + "wire_mark": "N-MISS", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "device-missing:terminal-a", + "end_element_uuid": "device-missing", + "end_terminal_display": "A1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + sample = report["missing_endpoint_samples"][0] + + self.assertEqual("instance-from-device-list", sample["end_instance_id"]) + self.assertEqual("UD:8", sample["end_device_label"]) + self.assertEqual( + {"device_not_in_3d_scene": 1}, + report["missing_terminal_summary"]["reason_code_counts"], + ) + self.assertEqual(1, len(report["missing_terminal_summary"]["device_groups"])) + self.assertEqual("UD:8", report["missing_terminal_summary"]["device_groups"][0]["device_label"]) + self.assertEqual(["A1"], report["missing_terminal_summary"]["device_groups"][0]["terminal_displays"]) + self.assertIn("UD:8", message) + self.assertIn("需补端子设备:UD:8 缺 1 处(A1)", message) + self.assertIn("instance=instance-from-device-list", message) + + def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_v2_payload_devices(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + payload = { + "schema_version": "2.0", + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-from-v2-device", + "display_tag": "UD:8", + "terminals": [ + { + "element_uuid": "device-missing", + "terminal_uuid": "terminal-a", + "terminal_instance_id": "instance-from-v2-terminal-a", + "terminal_display": "A1", + } + ], + } + ], + "wires": [ + { + "wire_id": "wire-1", + "wire_mark": "N-MISS", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-a", + "end_element_uuid": "device-missing", + "end_terminal_display": "A1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + sample = report["missing_endpoint_samples"][0] + + self.assertEqual("instance-from-v2-device", sample["end_instance_id"]) + self.assertEqual("UD:8", sample["end_device_label"]) + self.assertEqual("UD:8", report["missing_terminal_summary"]["device_groups"][0]["device_label"]) + self.assertEqual( + "instance-from-v2-device", + report["missing_terminal_summary"]["device_groups"][0]["instance_id"], + ) + + def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_context_json_devices(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + with tempfile.TemporaryDirectory() as temp_dir: + json_path = Path(temp_dir) / "2d_to_3d.json" + json_path.write_text( + json.dumps( + { + "project_uuid": "project-1", + "devices": [ + { + "element_uuid": "device-missing", + "instance_id": "instance-from-context-json", + "display_tag": "UD:8", + } + ], + "wires": [ + { + "wire_id": "wire-1", + "wire_mark": "N-MISS", + "wire_style_id": "1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "device-missing:terminal-a", + "end_element_uuid": "device-missing", + "end_terminal_display": "A1", + } + ], + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + app._qet_exchange_summary = {"json_path": str(json_path)} + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_mark": "N-MISS", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "device-missing:terminal-a", + "end_element_uuid": "device-missing", + "end_terminal_display": "A1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + sample = report["missing_endpoint_samples"][0] + self.assertEqual("instance-from-context-json", sample["end_instance_id"]) + self.assertEqual("UD:8", sample["end_device_label"]) + self.assertEqual( + "instance-from-context-json", + report["missing_terminal_summary"]["device_groups"][0]["instance_id"], + ) + self.assertTrue(report["context_devices_loaded"]) + self.assertEqual(1, report["context_device_count"]) + self.assertEqual(str(json_path), report["context_devices_json_path"]) + + def test_route_eplan_connections_from_payload_reports_device_without_terminals(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_without_terminals") + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-no-terminals") + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-no-terminals") + payload = { + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + "end_element_uuid": "device-no-terminals", + "end_instance_id": "instance-no-terminals", + "end_terminal_display": "A1", + } + ] + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + sample = report["missing_endpoint_samples"][0] + self.assertEqual("QETDevice_without_terminals", sample["end_device_name"]) + self.assertTrue(sample["end_device_in_scene"]) + self.assertEqual("no_3d_terminals_for_element", sample["end_missing_endpoint_reason_code"]) + self.assertIn("原因=该 2D 设备在 FreeCAD 中没有工程端子", message) + + def test_route_eplan_connections_treats_duplicate_terminal_uuid_without_matching_device_as_missing(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + for element_uuid, instance_id, terminal_uuid, x in ( + ("device-a", "instance-a", "shared-terminal", 10), + ("device-b", "instance-b", "shared-terminal", 20), + ("device-c", "instance-c", "other-terminal", 30), + ): + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_{0}".format(element_uuid)) + root.addObject(device) + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", element_uuid) + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", instance_id) + terminal_objects.ensure_string_property(device, "QetProjectUuid", "QET Exchange", "", "project-1") + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id=instance_id, + ) + terminal = terminal_objects.create_lcs_object( + doc, + "QETTerminal_{0}".format(instance_id), + placement=app.Placement(app.Vector(x, 0, 0), app.Rotation()), + label=terminal_uuid, + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + element_uuid, + terminal_uuid, + instance_id, + label=terminal_uuid, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-duplicate-context", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "shared-terminal", + "end_element_uuid": "device-c", + "end_instance_id": "instance-c", + "end_terminal_display": "1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + sample = report["missing_endpoint_samples"][0] + + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_terminal"]) + self.assertEqual(["shared-terminal"], report["missing_endpoint_uuids"]) + self.assertFalse(sample["end_found"]) + self.assertEqual("device-c", sample["end_element_uuid"]) + self.assertEqual("instance-c", sample["end_instance_id"]) + self.assertEqual(1, sample["end_element_terminal_count"]) + self.assertEqual(1, sample["end_instance_terminal_count"]) + self.assertEqual( + "terminal_uuid_not_in_element", + sample["end_missing_endpoint_reason_code"], + ) + + def test_preflight_treats_duplicate_terminal_uuid_without_matching_device_as_missing(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + for element_uuid, instance_id, terminal_uuid, x in ( + ("device-a", "instance-a", "shared-terminal", 10), + ("device-b", "instance-b", "shared-terminal", 20), + ("device-c", "instance-c", "other-terminal", 30), + ): + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_{0}".format(element_uuid)) + root.addObject(device) + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", element_uuid) + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", instance_id) + terminal_objects.ensure_string_property(device, "QetProjectUuid", "QET Exchange", "", "project-1") + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id=instance_id, + ) + terminal = terminal_objects.create_lcs_object( + doc, + "QETTerminal_preflight_{0}".format(instance_id), + placement=app.Placement(app.Vector(x, 0, 0), app.Rotation()), + label=terminal_uuid, + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + element_uuid, + terminal_uuid, + instance_id, + label=terminal_uuid, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-duplicate-context", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "shared-terminal", + "end_element_uuid": "device-c", + "end_instance_id": "instance-c", + "end_terminal_display": "1", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + sample = report["missing_endpoint_samples"][0] + + self.assertFalse(report["ok"]) + self.assertEqual(["shared-terminal"], report["missing_endpoint_uuids"]) + self.assertFalse(sample["end_found"]) + self.assertEqual("device-c", sample["end_element_uuid"]) + self.assertEqual("instance-c", sample["end_instance_id"]) + self.assertEqual( + "terminal_uuid_not_in_element", + sample["end_missing_endpoint_reason_code"], + ) + + def test_route_eplan_connections_from_payload_reports_missing_device_binding_metadata(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + payload = { + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + "end_terminal_display": "A1", + } + ] + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + sample = report["missing_endpoint_samples"][0] + self.assertEqual("missing_device_binding_metadata", sample["end_missing_endpoint_reason_code"]) + self.assertEqual("导线端点缺少 2D/3D 设备绑定信息", sample["end_missing_endpoint_reason_label"]) + self.assertIn("QET 导线端点缺少 element_uuid", message) + self.assertIn("第一版不要求 start/end_instance_id", message) + self.assertIn("原因=导线端点缺少 2D/3D 设备绑定信息", message) + + def test_route_eplan_connections_from_payload_applies_per_wire_required_route(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="普通路径", + project_uuid="project-1", + kind="UserPath", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="必经路径", + project_uuid="project-1", + kind="UserPath", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-required", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "required_route_carrier_labels": ["必经路径"], + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + self.assertIn("必经路径", labels) + self.assertNotIn("普通路径", labels) + + def test_route_eplan_connections_from_payload_applies_per_wire_required_source_name(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + direct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="普通路径", + project_uuid="project-1", + kind="UserPath", + ) + direct.QetRouteSourceName = "NormalSketch" + required = routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="黄色主路径", + project_uuid="project-1", + kind="UserPath", + ) + required.QetRouteSourceName = "RequiredSketch" + required.QetRouteSourceLabel = "黄色主路径草图" + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-required-source", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "required_route_carrier_source_names": ["RequiredSketch"], + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + self.assertIn("黄色主路径", labels) + self.assertNotIn("普通路径", labels) + self.assertEqual( + ["RequiredSketch"], + report["routes"][0]["network"]["route_constraints"]["required"]["source_names"], + ) + + def test_route_eplan_connections_from_payload_applies_per_wire_forbidden_route(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="禁止路径", + project_uuid="project-1", + kind="UserPath", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="允许路径", + project_uuid="project-1", + kind="UserPath", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-forbidden", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "forbidden_route_carrier_labels": ["禁止路径"], + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + self.assertIn("允许路径", labels) + self.assertNotIn("禁止路径", labels) + + def test_route_eplan_connections_from_payload_applies_per_wire_forbidden_source_name(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + forbidden = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="禁止源路径", + project_uuid="project-1", + kind="UserPath", + ) + forbidden.QetRouteSourceName = "ForbiddenSketch" + allowed = routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="允许源路径", + project_uuid="project-1", + kind="UserPath", + ) + allowed.QetRouteSourceName = "AllowedSketch" + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-forbidden-source", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "forbidden_route_carrier_source_names": ["ForbiddenSketch"], + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + self.assertIn("允许源路径", labels) + self.assertNotIn("禁止源路径", labels) + self.assertEqual( + ["ForbiddenSketch"], + report["routes"][0]["network"]["route_constraints"]["forbidden"]["source_names"], + ) + + def test_route_eplan_connections_from_payload_classifies_unsatisfied_route_constraints(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="普通路径", + project_uuid="project-1", + kind="UserPath", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-unsatisfied", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "required_route_carrier_labels": ["不存在的必经路径"], + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"terminal_exit_length": 0.0}, + ) + + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_route_network"]) + self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) + self.assertIn("路径约束", report["missing_route_network_samples"][0]["error"]) + + def test_route_eplan_connections_from_payload_skips_resolved_tasks_without_route_network(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-{0}".format(index), + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + for index in range(3) + ], + } + original_route = auto_routing.route_eplan_connection_between_terminals + + def fail_if_called(*_args, **_kwargs): + raise AssertionError("batch route must not call per-wire routing without route carriers") + + auto_routing.route_eplan_connection_between_terminals = fail_if_called + try: + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + finally: + auto_routing.route_eplan_connection_between_terminals = original_route + + self.assertEqual(0, report["routed"]) + self.assertEqual(3, report["skipped_missing_route_network"]) + self.assertEqual(3, report["route_status_counts"]["MissingRouteNetwork"]) + self.assertEqual([], report["errors"]) + self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + + def test_route_eplan_connection_tasks_marks_task_missing_route_network_when_skipped(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + task = wiring_objects.create_wire_task( + doc, + "project-1", + "wire-missing-network", + "N1", + "terminal-start", + "terminal-end", + "instance-a", + "instance-b", + ) + task.RouteStatus = "Routed" + + report = auto_routing.route_eplan_connection_tasks(doc) + + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_route_network"]) + self.assertEqual("MissingRouteNetwork", task.RouteStatus) + + def test_route_eplan_connection_tasks_auto_creates_diagnostic_bridge_before_routing(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板兜底路径", + ) + wiring_objects.create_wire_task( + doc, + "project-1", + "wire-task-bridge", + "N1", + "terminal-start", + "terminal-end", + "instance-a", + "instance-b", + ) + original_diagnostic = routing_network.diagnose_routing_path_network + original_create = routing_network.create_user_path_bridges_from_diagnostic_suggestions + calls = {"diagnostic": 0} + + def fake_diagnostic(*_args, **_kwargs): + calls["diagnostic"] += 1 + if calls["diagnostic"] == 1: + return { + "ok": False, + "issues": [ + { + "severity": "warning", + "code": "wire_ducts_without_terminal_access", + "count": 1, + }, + ], + "summary": {"carriers": 1}, + "wire_ducts_without_terminal_access": [ + { + "index": 0, + "carrier_names": ["孤立线槽"], + "bridge_suggestion": {"distance_mm": 40.0}, + }, + ], + } + return {"ok": True, "issues": [], "summary": {"carriers": 2}} + + def fake_create(_doc, _diagnostic, project_uuid=""): + carrier = routing_network.create_route_carrier( + _doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid=project_uuid or "project-1", + kind="WireDuct", + label="诊断桥接后主路径", + ) + return {"suggestions": 1, "created": [carrier], "duplicates": 0, "stale_suggestions": 0} + + routing_network.diagnose_routing_path_network = fake_diagnostic + routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create + try: + report = auto_routing.route_eplan_connection_tasks(doc) + finally: + routing_network.diagnose_routing_path_network = original_diagnostic + routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create + + self.assertEqual(1, report["auto_diagnostic_bridges"]["created_count"]) + self.assertEqual({"main_path_routes": 1, "fallback_routes": 0}, report["route_path_usage"]) + self.assertEqual(["Routed"], list(report["route_status_counts"].keys())) + self.assertNotIn("main_path_not_used", report["issue_codes"]) + + def test_eplan_connection_route_prefers_wire_duct_over_shorter_routing_range(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(300, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(300, 0, 20)], + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_route_carrier( + doc, + [ + app.Vector(0, 0, 20), + app.Vector(0, 1200, 20), + app.Vector(300, 1200, 20), + app.Vector(300, 0, 20), + ], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + + def test_eplan_connection_wire_records_fallback_route_quality_warning(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="安装板兜底路径", + project_uuid="project-1", + kind="RoutingRange", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + wire = result["wire"] + self.assertEqual("FallbackPathWarning", wire.QetRouteQualityStatus) + self.assertEqual("RoutingRange", wire.QetRouteFallbackCarrierKinds) + self.assertEqual("安装板兜底路径", wire.QetRouteFallbackCarrierLabels) + self.assertEqual("route_quality_warnings", wire.QetRouteIssueCodes) + self.assertEqual("路径质量告警", wire.QetRouteIssueLabels) + payload = json.loads(wire.QetRouteDiagnosticsJson) + self.assertEqual(["route_quality_warnings"], payload["issue_codes"]) + self.assertEqual(["路径质量告警"], payload["issue_labels"]) + self.assertEqual("FallbackPathWarning", payload["quality"]["quality_status"]) + self.assertEqual(["RoutingRange"], payload["quality"]["fallback_carrier_kinds"]) + + def test_eplan_connection_wire_records_third_party_collision_issue(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="线槽主路径", + project_uuid="project-1", + kind="WireDuct", + ) + obstacle = doc.addObject("Part::Feature", "ThirdPartyDevice") + obstacle.Label = "设备A" + terminal_objects.ensure_string_property( + obstacle, + "QetElementUuid", + "QET Exchange", + "", + "device-obstacle", + ) + obstacle.Shape = FakeShape(FakeBoundBox(50, 70, -10, 10, 15, 25)) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + endpoint_metadata={ + "start_element_uuid": "device-start", + "end_element_uuid": "device-end", + }, + ) + + wire = result["wire"] + self.assertIn("collision_warnings", wire.QetRouteIssueCodes) + self.assertIn("third_party_device_collisions", wire.QetRouteIssueCodes) + self.assertIn("第三方设备/布局碰撞", wire.QetRouteIssueLabels) + payload = json.loads(wire.QetRouteDiagnosticsJson) + self.assertIn("third_party_device_collisions", payload["issue_codes"]) + self.assertEqual( + "third_party_device_collision", + payload["collisions"][0]["collision_relation"], + ) + + def test_collision_relation_marks_endpoint_device_collision(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + + relation = auto_routing._collision_relation( + { + "obstacle_element_uuid": "device-start", + "start_element_uuid": "device-start", + "end_element_uuid": "device-end", + } + ) + + self.assertEqual("endpoint_device_collision", relation) + + def test_unbound_structural_collision_can_be_auto_ignored_without_ignoring_devices(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + + structural = { + "obstacle_label": "NFB BRACKET_P00", + "obstacle_name": "Solid043", + "obstacle_element_uuid": "", + "obstacle_parent_labels": ["CABINET ASS'Y", "QET Exchange Devices"], + "obstacle_parent_names": ["LinkGroup005", "QETExchangeDevices"], + } + device = { + "obstacle_label": "3S001", + "obstacle_name": "Device3S001", + "obstacle_element_uuid": "device-uuid", + "obstacle_parent_labels": ["QET Exchange Devices"], + "obstacle_parent_names": ["QETExchangeDevices"], + } + + self.assertTrue(auto_routing._is_auto_ignorable_unbound_structural_collision(structural)) + self.assertFalse(auto_routing._is_auto_ignorable_unbound_structural_collision(device)) + kept, ignored = auto_routing._filter_auto_ignored_collisions([structural, device]) + self.assertEqual([device], kept) + self.assertEqual([structural], ignored) + + def test_eplan_connection_route_prefers_wire_duct_when_routing_range_is_only_moderately_shorter(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(10, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_route_carrier( + doc, + [ + app.Vector(0, 0, 20), + app.Vector(0, 145, 20), + app.Vector(10, 145, 20), + app.Vector(10, 0, 20), + ], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + + def test_eplan_connection_route_considers_primary_entry_beyond_nearest_surface_candidates(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + for y in range(1, 11): + routing_network.create_route_carrier( + doc, + [app.Vector(0, y, 20), app.Vector(100, y, 20)], + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + + def test_route_eplan_connections_from_payload_skips_tasks_when_carriers_have_no_segments(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + broken_carrier = doc.addObject("Part::Feature", "BrokenCarrier") + terminal_objects.ensure_string_property( + broken_carrier, + "QetRoutingRole", + "QET Routing", + "Routing role marker", + "RoutingCarrier", + ) + terminal_objects.ensure_string_property( + broken_carrier, + "QetRouteCarrierKind", + "QET Routing", + "Route carrier kind", + "WireDuct", + ) + terminal_objects.ensure_bool_property( + broken_carrier, + "CanRouteWire", + "QET Routing", + "Whether routing connections can use this path", + True, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"network_entry_max_distance": 30.0}, + ) + + self.assertEqual(1, report["route_network_carriers"]) + self.assertEqual(0, report["route_network_segments"]) + self.assertEqual(0, report["route_network_nodes"]) + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_route_network"]) + self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) + self.assertEqual([], report["errors"]) + self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + + def test_route_eplan_connections_from_payload_applies_batch_entry_candidate_limit(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + captured_options = [] + original = auto_routing.route_eplan_connection_between_terminals + + def fake_route(*args, **kwargs): + captured_options.append(dict(kwargs.get("options") or {})) + return { + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": {}, + "route_track": {}, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a", + } + + auto_routing.route_eplan_connection_between_terminals = fake_route + try: + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={ + "network_entry_candidate_limit": 8, + "batch_network_entry_candidate_limit": 2, + "batch_network_entry_total_candidate_limit": 4, + }, + ) + finally: + auto_routing.route_eplan_connection_between_terminals = original + + self.assertEqual(1, report["routed"]) + self.assertEqual(2, report["batch_network_entry_candidate_limit"]) + self.assertEqual(4, report["batch_network_entry_total_candidate_limit"]) + self.assertFalse(report["batch_avoid_obstacles"]) + self.assertEqual(2, captured_options[0]["network_entry_candidate_limit"]) + self.assertEqual(4, captured_options[0]["network_entry_candidate_total_limit"]) + self.assertFalse(captured_options[0]["avoid_obstacles"]) + self.assertIsInstance(captured_options[0]["__base_route_network"], dict) + self.assertIsInstance(captured_options[0]["__obstacle_candidate_cache"], dict) + + def test_route_eplan_connections_retries_missing_route_with_wider_candidate_limit(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + captured_limits = [] + original = auto_routing.route_eplan_connection_between_terminals + + def fake_route(*args, **kwargs): + limit = int((kwargs.get("options") or {}).get("network_entry_candidate_limit", 0) or 0) + captured_limits.append(limit) + if limit < 8: + raise auto_routing.AutoRoutingError( + "没有可用的布线路径网络;请先生成布线布局空间和布线路径网络。" + ) + return { + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": {}, + "route_track": {}, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a", + } + + auto_routing.route_eplan_connection_between_terminals = fake_route + try: + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={ + "network_entry_candidate_limit": 8, + "batch_network_entry_candidate_limit": 3, + "missing_route_retry_candidate_limit": 8, + }, + ) + finally: + auto_routing.route_eplan_connection_between_terminals = original + + self.assertEqual([3, 8], captured_limits) + self.assertEqual(1, report["routed"]) + self.assertEqual(0, report["skipped_missing_route_network"]) + self.assertEqual(1, report["missing_route_retries"]) + self.assertEqual(1, report["route_status_counts"]["Routed"]) + + def test_route_eplan_connections_selectively_reroutes_third_party_collisions(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_element_uuid": "device-start", + "start_terminal_uuid": "terminal-start", + "end_element_uuid": "device-end", + "end_terminal_uuid": "terminal-end", + }, + ], + } + captured_avoid = [] + original = auto_routing.route_eplan_connection_between_terminals + + def fake_route(*args, **kwargs): + route_options = dict(kwargs.get("options") or {}) + avoid = bool(route_options.get("avoid_obstacles", False)) + captured_avoid.append(avoid) + if avoid: + return { + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 12.0, + "lane": {"index": 0}, + "network": {}, + "route_track": {}, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a clean", + } + return { + "algorithm": "fake", + "route_status": "CollisionWarning", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": {}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} + ] + }, + "collision_count": 1, + "collisions": [ + { + "collision_kind": "HardIntersection", + "obstacle_element_uuid": "device-obstacle", + "obstacle_label": "设备A", + } + ], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a collision", + } + + auto_routing.route_eplan_connection_between_terminals = fake_route + try: + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + finally: + auto_routing.route_eplan_connection_between_terminals = original + + self.assertEqual([False, True], captured_avoid) + self.assertEqual(1, report["selective_collision_reroute_attempts"]) + self.assertEqual(1, report["selective_collision_reroutes"]) + self.assertEqual(0, report["selective_collision_reroute_no_improvement"]) + self.assertEqual(1, report["routed"]) + self.assertEqual(0, report["collision_warnings"]) + self.assertEqual("Routed", report["routes"][0]["route_status"]) + + def test_route_eplan_connections_rejects_selective_reroute_when_it_uses_fallback_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_element_uuid": "device-start", + "start_terminal_uuid": "terminal-start", + "end_element_uuid": "device-end", + "end_terminal_uuid": "terminal-end", + }, + ], + } + original = auto_routing.route_eplan_connection_between_terminals + created_wires = [] + + def fake_route(*args, **kwargs): + route_doc = args[0] + avoid = bool((kwargs.get("options") or {}).get("avoid_obstacles", False)) + if avoid: + retry_wire = route_doc.addObject("Part::Feature", "RetryFallbackWire") + created_wires.append(retry_wire) + return { + "wire": retry_wire, + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 12.0, + "lane": {"index": 0}, + "network": {}, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "辅助面"}} + ] + }, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a fallback", + } + original_wire = route_doc.addObject("Part::Feature", "OriginalCollisionWire") + created_wires.append(original_wire) + return { + "wire": original_wire, + "algorithm": "fake", + "route_status": "CollisionWarning", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": {}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} + ] + }, + "collision_count": 1, + "collisions": [ + { + "collision_kind": "HardIntersection", + "obstacle_element_uuid": "device-obstacle", + "obstacle_label": "设备A", + } + ], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a collision", + } + + auto_routing.route_eplan_connection_between_terminals = fake_route + try: + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + finally: + auto_routing.route_eplan_connection_between_terminals = original + + self.assertEqual(1, report["selective_collision_reroute_attempts"]) + self.assertEqual(0, report["selective_collision_reroutes"]) + self.assertEqual(1, report["selective_collision_reroute_rejected_fallback"]) + self.assertEqual(1, report["collision_warnings"]) + self.assertEqual("CollisionWarning", report["routes"][0]["route_status"]) + self.assertEqual("RejectedFallback", report["routes"][0]["selective_collision_reroute_status"]) + self.assertEqual( + ["RoutingRange"], + report["routes"][0]["selective_collision_reroute_rejected_fallback_kinds"], + ) + self.assertEqual( + ["辅助面"], + report["routes"][0]["selective_collision_reroute_rejected_fallback_labels"], + ) + self.assertIn("main_path_detour_missing", report["routes"][0]["issue_codes"]) + compact = auto_routing._compact_routing_connection_batch_report(report) + self.assertIn("main_path_detour_missing", compact["route_samples"][0]["issue_codes"]) + self.assertEqual( + ["辅助面"], + compact["route_samples"][0]["selective_collision_reroute"]["rejected_fallback_labels"], + ) + self.assertEqual(1, report["main_path_detour_missing_summary"]["wire_count"]) + self.assertEqual( + {"辅助面": 1}, + report["main_path_detour_missing_summary"]["rejected_fallback_label_counts"], + ) + self.assertEqual( + {"主线槽A": 1}, + report["main_path_detour_missing_summary"]["current_route_source_label_counts"], + ) + self.assertEqual( + {"辅助面 -> 主线槽A": 1}, + report["main_path_detour_missing_summary"]["bridge_pair_counts"], + ) + self.assertEqual( + ["点击“选择缺主路径补路位置”快速定位汇总需补区域"], + [ + action + for action in report["recommended_actions"] + if "选择缺主路径补路位置" in action + ], + ) + self.assertIn("main_path_detour_missing", created_wires[0].QetRouteIssueCodes) + wire_payload = json.loads(created_wires[0].QetRouteDiagnosticsJson) + self.assertEqual( + ["辅助面"], + wire_payload["selective_collision_reroute"]["rejected_fallback_labels"], + ) + self.assertIn("main_path_detour_missing", report["issue_codes"]) + message = auto_routing.format_eplan_connection_route_report(report) + self.assertIn("局部避障:尝试 1 条,接受 0 条,拒绝辅助路径 1 条", message) + self.assertIn("请补主路径/UserPath 或调整装配", message) + self.assertIn("缺主路径绕行:1 条,需补路径位置:辅助面 1 条", message) + self.assertIn("辅助面 -> 主线槽A 1 条", message) + + def test_route_eplan_connections_auto_bridges_main_path_detour_pairs_by_default_and_retries_once(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") + fallback_source.Label = "门板布线面" + current_source = doc.addObject("Part::Feature", "MainDuctSource") + current_source.Label = "主线槽A" + fallback_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="门板布线面 carrier", + ) + current_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(140, 20, 20), app.Vector(240, 20, 20)], + project_uuid="project-1", + kind="WireDuct", + label="主线槽A carrier", + ) + fallback_carrier.QetRouteSourceName = fallback_source.Name + fallback_carrier.QetRouteSourceLabel = fallback_source.Label + current_carrier.QetRouteSourceName = current_source.Name + current_carrier.QetRouteSourceLabel = current_source.Label + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_element_uuid": "device-start", + "start_terminal_uuid": "terminal-start", + "end_element_uuid": "device-end", + "end_terminal_uuid": "terminal-end", + }, + ], + } + original = auto_routing.route_eplan_connection_between_terminals + calls = [] + + def fake_route(*args, **kwargs): + route_doc = args[0] + calls.append(bool((kwargs.get("options") or {}).get("avoid_obstacles", False))) + detour_path_exists = any( + getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourPath" + for carrier in routing_network.collect_route_carriers(route_doc) + ) + wire = route_doc.addObject("Part::Feature", "WireAfterDetourPath" if detour_path_exists else "WireBeforeDetourPath") + if detour_path_exists: + return { + "wire": wire, + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": {}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} + ] + }, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a routed", + } + if bool((kwargs.get("options") or {}).get("avoid_obstacles", False)): + points = [ + app.Vector(0, 0, 0), + app.Vector(0, 0, 20), + app.Vector(80, 0, 20), + app.Vector(140, 20, 20), + app.Vector(100, 0, 0), + ] + wire.Points = points + return { + "wire": wire, + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 12.0, + "lane": {"index": 0}, + "network": {}, + "points": points, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} + ] + }, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a fallback", + } + return { + "wire": wire, + "algorithm": "fake", + "route_status": "CollisionWarning", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": {}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} + ] + }, + "collision_count": 1, + "collisions": [ + { + "collision_kind": "HardIntersection", + "obstacle_element_uuid": "device-obstacle", + "obstacle_label": "设备A", + } + ], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a collision", + } + + auto_routing.route_eplan_connection_between_terminals = fake_route + try: + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + project_uuid="project-1", + update_network=False, + ) + finally: + auto_routing.route_eplan_connection_between_terminals = original + + bridges = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourBridge" + ] + detour_paths = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourPath" + ] + + self.assertEqual(1, report["routed"]) + self.assertEqual(0, report["collision_warnings"]) + self.assertEqual({"Routed": 1}, report["route_status_counts"]) + self.assertEqual(1, report["auto_main_path_detour_bridges"]["created_count"]) + self.assertTrue(report["auto_main_path_detour_bridges"]["rerouted"]) + self.assertEqual(1, report["auto_main_path_detour_bridges"]["retry_wires"]) + self.assertEqual(1, report["auto_main_path_detour_bridges"]["retry_replaced_routes"]) + self.assertEqual("门板布线面 -> 主线槽A", bridges[0].QetRouteBridgePairLabel) + self.assertEqual("门板布线面 -> 主线槽A", detour_paths[0].QetRouteBridgePairLabel) + self.assertEqual([False, True, False], calls) + compact = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) + self.assertEqual(1, compact["auto_main_path_detour_bridges"]["created_count"]) + self.assertIn("自动主路径补桥:生成 UserPath 1 条并重跑布线", message) + + def test_route_eplan_connections_auto_bridges_terminal_access_fallback_targets_by_default_and_retries_once(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + fallback_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面", + ) + main_path = routing_network.create_route_carrier( + doc, + [app.Vector(130, 20, 20), app.Vector(230, 20, 20)], + project_uuid="project-1", + kind="UserPath", + label="柜内主路径", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_element_uuid": "device-start", + "start_terminal_uuid": "terminal-start", + "end_element_uuid": "device-end", + "end_terminal_uuid": "terminal-end", + }, + ], + } + original = auto_routing.route_eplan_connection_between_terminals + calls = [] + + def fake_route(*args, **kwargs): + route_doc = args[0] + calls.append("route") + bridge_exists = any( + getattr(carrier, "QetRouteBridgeKind", "") == "TerminalAccessFallbackBridge" + for carrier in routing_network.collect_route_carriers(route_doc) + ) + wire = route_doc.addObject( + "Part::Feature", + "WireAfterTerminalAccessBridge" if bridge_exists else "WireBeforeTerminalAccessBridge", + ) + if bridge_exists: + return { + "wire": wire, + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 10.0, + "lane": {"index": 0}, + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + "start_terminal_access_target_kind": "UserPath", + "start_terminal_access_target_name": main_path.Name, + "start_terminal_access_target_label": "柜内主路径", + "start_terminal_access_target_distance": 20.0, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "UserPath", "name": main_path.Name, "label": "柜内主路径"}} + ] + }, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a routed", + } + return { + "wire": wire, + "algorithm": "fake", + "route_status": "Routed", + "length_mm": 12.0, + "lane": {"index": 0}, + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + "start_terminal_access_target_kind": "RoutingRange", + "start_terminal_access_target_name": fallback_carrier.Name, + "start_terminal_access_target_label": "安装板布线面", + "start_terminal_access_target_distance": 35.0, + "end_terminal_access_target_kind": "UserPath", + "end_terminal_access_target_name": main_path.Name, + "end_terminal_access_target_label": "柜内主路径", + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "name": fallback_carrier.Name, "label": "安装板布线面"}} + ] + }, + "collision_count": 0, + "collisions": [], + "wire_style_status": "NotRequested", + "wire_object_label": "wire-a fallback", + } + + auto_routing.route_eplan_connection_between_terminals = fake_route + try: + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + project_uuid="project-1", + update_network=False, + ) + finally: + auto_routing.route_eplan_connection_between_terminals = original + + bridges = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "TerminalAccessFallbackBridge" + ] + self.assertEqual(1, report["routed"]) + self.assertNotIn("terminal_access_fallback_targets", report["issue_codes"]) + self.assertEqual(1, len(bridges)) + self.assertEqual(1, report["auto_terminal_access_fallback_bridges"]["created_count"]) + self.assertTrue(report["auto_terminal_access_fallback_bridges"]["rerouted"]) + self.assertEqual(1, report["auto_terminal_access_fallback_bridges"]["retry_wires"]) + self.assertEqual(1, report["auto_terminal_access_fallback_bridges"]["retry_replaced_routes"]) + self.assertEqual( + ["安装板布线面 -> 柜内主路径"], + report["auto_terminal_access_fallback_bridges"]["created_pair_labels"], + ) + self.assertEqual(["route", "route"], calls) + compact = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) + self.assertEqual(1, compact["auto_terminal_access_fallback_bridges"]["created_count"]) + self.assertEqual( + ["安装板布线面 -> 柜内主路径"], + compact["auto_terminal_access_fallback_bridges"]["created_pair_labels"], + ) + self.assertIn("自动端子接入补桥:生成 UserPath 1 条并重跑布线", message) + self.assertIn("安装板布线面 -> 柜内主路径", message) + + def test_auto_main_path_detour_user_path_raises_capacity_when_same_path_reused(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + points = [ + app.Vector(0, 0, 0), + app.Vector(0, 0, 20), + app.Vector(100, 0, 20), + app.Vector(100, 0, 0), + ] + retry_result = { + "points": points, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} + ] + }, + } + original_result = { + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} + ] + }, + } + + first = auto_routing._create_main_path_detour_user_path_from_retry( + doc, + retry_result, + original_result, + project_uuid="project-1", + ) + second = auto_routing._create_main_path_detour_user_path_from_retry( + doc, + retry_result, + original_result, + project_uuid="project-1", + ) + + self.assertIs(first, second) + self.assertEqual(2, first.QetRouteCarrierCapacity) + + def test_auto_main_path_detour_user_path_initial_capacity_matches_lane_parallel_count(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + retry_result = { + "points": [ + app.Vector(0, 0, 0), + app.Vector(0, 0, 20), + app.Vector(100, 0, 20), + app.Vector(100, 0, 0), + ], + "lane": {"index": 1}, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} + ] + }, + } + original_result = { + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} + ] + }, + } + + carrier = auto_routing._create_main_path_detour_user_path_from_retry( + doc, + retry_result, + original_result, + project_uuid="project-1", + ) + + self.assertEqual(2, carrier.QetRouteCarrierCapacity) + + def test_route_report_raises_auto_detour_path_capacity_from_final_lane_usage(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="UserPath", + capacity=1, + ) + carrier.QetRouteBridgeKind = "MainPathDetourPath" + report = { + "routes": [ + { + "wire_uuid": "wire-auto-detour", + "route_status": "Routed", + "lane": {"index": 1}, + "route_track": { + "segments": [ + { + "carrier": { + "name": carrier.Name, + "kind": "UserPath", + "capacity": 1, + } + } + ] + }, + } + ], + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, + "skipped_invalid": 0, + "errors": [], + } + + auto_routing._raise_main_path_detour_capacities_from_report(doc, report) + + self.assertEqual(2, carrier.QetRouteCarrierCapacity) + self.assertEqual(2, report["routes"][0]["route_track"]["segments"][0]["carrier"]["capacity"]) + self.assertEqual([], auto_routing._route_capacity_pressure_samples(report, limit=0)) + + def test_collect_obstacles_cache_preserves_endpoint_filters(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + + endpoint_body = doc.addObject("Part::Feature", "EndpointBody") + endpoint_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 5)) + endpoint_body.QetInstanceId = terminal.QetInstanceId + + near_body = doc.addObject("Part::Feature", "NearBody") + near_body.Shape = FakeShape(FakeBoundBox(1, 2, -1, 1, -1, 1)) + + far_body = doc.addObject("Part::Feature", "FarBody") + far_body.Shape = FakeShape(FakeBoundBox(80, 90, -1, 1, -1, 1)) + + options = {"terminal_exit_length": 20.0, "obstacle_clearance": 0.0} + uncached = auto_routing.collect_obstacles(doc, exclude=[terminal], options=options) + cache = auto_routing._obstacle_candidate_cache(doc, options=options) + cached = auto_routing.collect_obstacles( + doc, + exclude=[terminal], + options=dict(options, __obstacle_candidate_cache=cache), + ) + + self.assertEqual(["FarBody"], [item["name"] for item in uncached]) + self.assertEqual(["FarBody"], [item["name"] for item in cached]) + + def test_collect_obstacles_skips_parent_of_support_surface_source(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + parent = doc.addObject("App::LinkGroup", "DoorAssembly") + parent.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) + panel = doc.addObject("Part::Feature", "DoorPanel") + panel.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 2)) + panel.QetRoutingObstacleMode = "SupportSurface" + parent.addObject(panel) + obstacle = doc.addObject("Part::Feature", "DeviceObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) + + obstacles = auto_routing.collect_obstacles(doc) + cache = auto_routing._obstacle_candidate_cache(doc) + cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + + self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) + self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + + def test_collect_obstacles_skips_descendant_of_pass_through_ancestor(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + assembly = doc.addObject("App::LinkGroup", "DoorAssembly") + assembly.QetRoutingObstacleMode = "PassThrough" + compound = doc.addObject("Part::Compound2", "DoorCompound") + panel = doc.addObject("Part::Feature", "DoorPanel") + panel.Shape = FakeShape(FakeBoundBox(0, 40, 0, 40, 0, 40)) + assembly.addObject(compound) + compound.addObject(panel) + obstacle = doc.addObject("Part::Feature", "DeviceObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) + + obstacles = auto_routing.collect_obstacles(doc) + cache = auto_routing._obstacle_candidate_cache(doc) + cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + + self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) + self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + + def test_collect_obstacles_reports_full_parent_chain_for_nested_import_parts(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + assembly = doc.addObject("App::LinkGroup", "DoorAssembly") + assembly.Label = "FRONT DOOR-R ASS'Y" + compound = doc.addObject("Part::Compound2", "DoorCompound") + compound.Label = "NAUO141" + panel = doc.addObject("Part::Feature", "DoorPanel") + panel.Shape = FakeShape(FakeBoundBox(0, 40, 0, 40, 0, 40)) + assembly.addObject(compound) + compound.addObject(panel) + + obstacles = auto_routing.collect_obstacles(doc) + + self.assertEqual(["DoorPanel"], [item["name"] for item in obstacles]) + self.assertEqual(["DoorCompound", "DoorAssembly"], obstacles[0]["parent_refs"]["names"]) + self.assertEqual(["NAUO141", "FRONT DOOR-R ASS'Y"], obstacles[0]["parent_refs"]["labels"]) + + def test_collect_obstacles_skips_auto_detected_support_surface_candidate(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + side_cover = doc.addObject("Part::Feature", "SideCover") + side_cover.Label = "SIDE COVER-1_P00" + side_cover.Shape = FakeShape(FakeBoundBox(0, 600, 0, 2148, 0, 30)) + obstacle = doc.addObject("Part::Feature", "DeviceObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) + + obstacles = auto_routing.collect_obstacles(doc) + cache = auto_routing._obstacle_candidate_cache(doc) + cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + + self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) + self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + + def test_collect_obstacles_skips_outlist_ancestor_of_support_surface_source(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + parent = doc.addObject("App::LinkGroup", "DoorAssembly") + parent.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) + compound = doc.addObject("Part::Compound2", "DoorCompound") + compound.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) + panel = doc.addObject("Part::Feature", "DoorPanel") + panel.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 2)) + panel.QetRoutingObstacleMode = "SupportSurface" + parent.OutList = [compound] + compound.InList = [parent] + compound.OutList = [panel] + panel.InList = [compound] + obstacle = doc.addObject("Part::Feature", "DeviceObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) + + obstacles = auto_routing.collect_obstacles(doc) + cache = auto_routing._obstacle_candidate_cache(doc) + cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + + self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) + self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + + def test_route_eplan_connections_classifies_disconnected_network_as_missing_route_network(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 20), app.Vector(1010, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "wire_label": "N4111", + "start_terminal_uuid": "terminal-start", + "start_element_uuid": "QF1", + "start_terminal_display": "A1", + "end_terminal_uuid": "terminal-end", + "end_element_uuid": "KM1", + "end_terminal_display": "13", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"network_entry_max_distance": 30.0}, + ) + + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_route_network"]) + self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) + self.assertEqual([], report["errors"]) + self.assertEqual("wire-a", report["missing_route_network_samples"][0]["wire_uuid"]) + self.assertEqual("N4111", report["missing_route_network_samples"][0]["wire_object_label"]) + self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + + def test_route_eplan_connections_from_payload_attaches_path_diagnostic_when_network_missing(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-no-network", + "wire_label": "N-NET", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_route_network"]) + self.assertIn("routing_path_network_diagnostic", report) + self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) + self.assertTrue(report["routing_path_network_diagnostic"]["issue_codes"]) + self.assertEqual(0, report["routing_sources"]["candidate_sources"]) + self.assertEqual(0, report["routing_sources"]["route_carriers"]) + self.assertIn("路径网络检查提示", message) + self.assertIn("未识别到线槽、布线面或用户路径源", message) + + def test_route_eplan_connections_from_payload_reports_sources_not_generated(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + panel = doc.addObject("Part::Feature", "MarkedRoutingSource") + panel.Label = "已标记布线面" + panel.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 5)) + panel.QetRoutingSourceKind = "RoutingRange" + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-source-only", + "wire_label": "N-SRC", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["skipped_missing_route_network"]) + self.assertEqual(1, report["routing_sources"]["candidate_sources"]) + self.assertEqual(0, report["routing_sources"]["route_carriers"]) + self.assertEqual({"RoutingRange": 1}, report["routing_sources"]["marked_source_counts"]) + self.assertIn("已识别到布线源 1 个,但还没有生成可用路径 carrier", message) + + def test_network_entry_uses_terminal_access_max_distance_when_smaller(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(500, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + route = auto_routing.build_network_route( + start, + end, + options={"terminal_access_max_distance": 30.0}, + doc=doc, + ) + + self.assertIsNone(route) + + def test_route_eplan_connections_writes_diagnostic_object_for_missing_terminal(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + + self.assertEqual(1, report["skipped_missing_terminal"]) + self.assertIsNotNone(diagnostic_group) + self.assertEqual(1, len(diagnostic_group.Group)) + diagnostic = diagnostic_group.Group[0] + self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) + self.assertEqual("project-1", diagnostic.QetProjectUuid) + self.assertFalse(diagnostic.QetDiagnosticOk) + self.assertIn("missing_terminals", diagnostic.QetDiagnosticIssueCodes) + self.assertIn("端子匹配失败", diagnostic.QetDiagnosticIssueLabels) + self.assertIn("批量生成布线连接完成", diagnostic.QetDiagnosticMessage) + self.assertIn("缺失端子 1 条", diagnostic.QetDiagnosticMessage) + self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) + + def test_route_eplan_connections_writes_diagnostic_object_when_no_wire_tasks(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + payload = {"project_uuid": "project-1", "wires": []} + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + + self.assertEqual(0, report["total_wires"]) + self.assertIn("no_wire_tasks", report["issue_codes"]) + self.assertIsNotNone(diagnostic_group) + self.assertEqual(1, len(diagnostic_group.Group)) + diagnostic = diagnostic_group.Group[0] + self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) + self.assertEqual("project-1", diagnostic.QetProjectUuid) + self.assertFalse(diagnostic.QetDiagnosticOk) + self.assertIn("routed=0", diagnostic.QetDiagnosticMessage) + self.assertIn("没有导线任务", diagnostic.QetDiagnosticMessage) + diagnostic_payload = json.loads(diagnostic.QetDiagnosticJson) + self.assertEqual(0, diagnostic_payload["total_wires"]) + self.assertIn("no_wire_tasks", diagnostic_payload["issue_codes"]) + + def test_route_eplan_connections_writes_compact_batch_diagnostic(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 20, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 20, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "wire_label": "N2", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + + self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, report["runtime_version"]) + self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, diagnostic_payload["runtime_version"]) + self.assertEqual(2, len(report["routes"])) + self.assertNotIn("routes", diagnostic_payload) + self.assertEqual(2, diagnostic_payload["route_sample_count"]) + self.assertEqual(2, len(diagnostic_payload["route_samples"])) + self.assertEqual("wire-a", diagnostic_payload["route_samples"][0]["wire_uuid"]) + self.assertEqual("Routed", diagnostic_payload["route_samples"][0]["route_status"]) + + def test_compact_batch_report_prioritizes_problem_route_samples(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 3, + "routed": 3, + "routes": [ + {"wire_uuid": "normal-a", "route_status": "Routed"}, + {"wire_uuid": "normal-b", "route_status": "Routed"}, + { + "wire_uuid": "problem-collision", + "route_status": "CollisionWarning", + "collisions": [ + { + "collision_kind": "HardIntersection", + "collision_relation": "third_party_device_collision", + } + ], + }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report( + report, + sample_limit=2, + ) + + self.assertEqual(3, payload["route_count"]) + self.assertEqual(2, payload["route_sample_count"]) + self.assertEqual("problem-collision", payload["route_samples"][0]["wire_uuid"]) + self.assertEqual( + ["collision_warnings", "hard_intersections", "third_party_device_collisions"], + payload["route_samples"][0]["issue_codes"], + ) + + def test_compact_route_sample_distinguishes_clearance_and_hard_collision_issues(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + + clearance_sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-clearance", + "route_status": "CollisionWarning", + "collisions": [ + { + "collision_kind": "ClearanceWarning", + "collision_relation": "third_party_device_collision", + } + ], + } + ) + hard_sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-hard", + "route_status": "CollisionWarning", + "collisions": [ + { + "collision_kind": "HardIntersection", + "collision_relation": "third_party_device_collision", + } + ], + } + ) + + self.assertIn("clearance_warnings", clearance_sample["issue_codes"]) + self.assertNotIn("hard_intersections", clearance_sample["issue_codes"]) + self.assertIn("hard_intersections", hard_sample["issue_codes"]) + self.assertNotIn("clearance_warnings", hard_sample["issue_codes"]) + + def test_compact_route_sample_includes_wire_object_label(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-label", + "wire_label": "N4111", + "wire_object_label": "N4111: terminal-start -> terminal-end (Routed)", + } + ) + + self.assertEqual( + "N4111: terminal-start -> terminal-end (Routed)", + sample["wire_object_label"], + ) + + def test_compact_route_sample_prefers_route_track_bridged_segment_count(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-bridge", + "route_track": { + "bridged_segments": 1, + }, + "network": { + "bridged_segments": 3, + }, + } + ) + + self.assertEqual(1, sample["network"]["bridged_segments"]) + + def test_compact_route_sample_includes_terminal_access_consumption(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-terminal-access", + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": False, + "start_terminal_access_carrier": "QETRouteCarrier_start", + "end_terminal_access_carrier": "", + "start_terminal_access_label": "起点接入", + "end_terminal_access_label": "", + "start_terminal_access_target_kind": "UserPath", + "start_terminal_access_target_label": "柜内主路径", + "start_terminal_access_target_distance": 100.0, + "start_terminal_access_target_component_primary_segments": 4, + }, + "route_track": {"segments": []}, + } + ) + + self.assertEqual( + { + "start_consumed": True, + "end_consumed": False, + "start_carrier": "QETRouteCarrier_start", + "end_carrier": "", + "start_label": "起点接入", + "end_label": "", + "start_target_kind": "UserPath", + "start_target_name": "", + "start_target_label": "柜内主路径", + "start_target_distance": 100.0, + "start_target_component_primary_segments": 4, + "end_target_kind": "", + "end_target_name": "", + "end_target_label": "", + "end_target_distance": 0.0, + "end_target_component_primary_segments": 0, + }, + sample["network"]["terminal_access"], + ) + + def test_compact_route_sample_includes_candidate_obstacle_hits(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-obstacle-entry", + "network": { + "entry_candidate_rank": 3, + "exit_candidate_rank": 2, + "entry_candidate_score": 125.0, + "route_candidate_obstacle_hits": 2, + }, + } + ) + + self.assertEqual(3, sample["network"]["entry_candidate_rank"]) + self.assertEqual(2, sample["network"]["exit_candidate_rank"]) + self.assertEqual(125.0, sample["network"]["entry_candidate_score"]) + self.assertEqual(2, sample["network"]["route_candidate_obstacle_hits"]) + + def test_compact_route_sample_includes_candidate_boundary_metadata(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-boundary", + "network": { + "boundary_aware": True, + "route_candidate_boundary_violations": 2, + }, + } + ) + + self.assertTrue(sample["network"]["boundary_aware"]) + self.assertEqual(2, sample["network"]["route_candidate_boundary_violations"]) + + def test_compact_route_sample_includes_single_wire_status_summaries(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-status-summary", + "collisions": [ + {"collision_kind": "HardIntersection"}, + {"collision_kind": "ClearanceWarning"}, + ], + "lane": {"index": 2, "spacing_mm": 10.0, "axis": "y"}, + "network": { + "entry_distance": 125.0, + "exit_distance": 20.0, + "terminal_access_warning_distance": 100.0, + "boundary_aware": True, + "route_candidate_boundary_violations": 1, + }, + "route_track": { + "segments": [ + { + "carrier": { + "kind": "RoutingRange", + "label": "安装板兜底路径", + "capacity": 1, + } + } + ] + }, + } + ) + + self.assertEqual("LongAccessWarning", sample["access"]["access_status"]) + self.assertEqual(["entry"], sample["access"]["warning_sides"]) + self.assertEqual("HardIntersectionWarning", sample["collision_summary"]["collision_status"]) + self.assertEqual(1, sample["collision_summary"]["hard_intersection_count"]) + self.assertEqual(1, sample["collision_summary"]["clearance_warning_count"]) + self.assertEqual("FallbackPathWarning", sample["quality"]["quality_status"]) + self.assertEqual(["RoutingRange"], sample["quality"]["fallback_carrier_kinds"]) + self.assertEqual("CapacityWarning", sample["capacity"]["capacity_status"]) + self.assertEqual(3, sample["capacity"]["parallel_wire_count"]) + self.assertEqual("BoundaryWarning", sample["boundary"]["boundary_status"]) + self.assertEqual( + [ + "long_terminal_access", + "collision_warnings", + "hard_intersections", + "clearance_warnings", + "route_quality_warnings", + "route_capacity_pressure", + "route_candidate_boundary_violations", + ], + sample["issue_codes"], + ) + self.assertIn("端子接入过长", sample["issue_labels"]) + self.assertIn("碰撞告警", sample["issue_labels"]) + self.assertIn("硬穿模", sample["issue_labels"]) + self.assertIn("间隙不足", sample["issue_labels"]) + + def test_compact_route_sample_includes_route_constraints(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-constraints", + "network": { + "route_constraints": { + "required": {"labels": ["必经路径"]}, + "forbidden": {"labels": ["禁止路径"]}, + }, + }, + } + ) + + self.assertEqual( + ["必经路径"], + sample["network"]["route_constraints"]["required"]["labels"], + ) + self.assertEqual( + ["禁止路径"], + sample["network"]["route_constraints"]["forbidden"]["labels"], + ) + + def test_compact_route_sample_formats_user_path_source_index(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-source-index", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "UserPath", + "source_label": "多路径草图", + "source_path_index": "1", + } + }, + { + "carrier": { + "kind": "UserPath", + "source_label": "多路径草图", + "source_path_index": "2", + } + }, + ] + }, + } + ) - def test_route_eplan_connections_from_payload_attaches_path_diagnostic_when_network_missing(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-no-network", - "wire_label": "N-NET", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } - ], - } + self.assertEqual( + ["多路径草图(路径1)", "多路径草图(路径2)"], + sample["route_source_labels"], + ) - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) + def test_compact_route_sample_ignores_bridge_only_carrier_summary(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + sample = auto_routing._compact_route_sample( + { + "wire_uuid": "wire-bridge", + "route_track": { + "carrier_kinds": {"RoutingRange": 1}, + "carrier_names": ["VirtualBridge"], + "segments": [ + { + "is_bridge": True, + "carrier": {"name": "VirtualBridge", "kind": "RoutingRange"}, + }, + { + "carrier": {"name": "WireDuctA", "kind": "WireDuct"}, + }, + ], + }, + } + ) - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertIn("routing_path_network_diagnostic", report) - self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) - self.assertTrue(report["routing_path_network_diagnostic"]["issue_codes"]) - self.assertEqual(0, report["routing_sources"]["candidate_sources"]) - self.assertEqual(0, report["routing_sources"]["route_carriers"]) - self.assertIn("路径网络检查提示", message) - self.assertIn("未识别到线槽、布线面或用户路径源", message) + self.assertEqual({"WireDuct": 1}, sample["carrier_kinds"]) + self.assertEqual(["WireDuctA"], sample["carrier_names"]) - def test_route_eplan_connections_from_payload_reports_sources_not_generated(self): + def test_route_eplan_connections_batch_diagnostic_includes_quality_warnings(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - panel = doc.addObject("Part::Feature", "MarkedRoutingSource") - panel.Label = "已标记布线面" - panel.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 5)) - panel.QetRoutingSourceKind = "RoutingRange" + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="RoutingRange", + ) payload = { "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-source-only", - "wire_label": "N-SRC", + "wire_id": "wire-surface", + "wire_label": "N-SURFACE", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } @@ -10323,821 +13231,890 @@ class AutoRoutingTest(unittest.TestCase): } report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) - - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertEqual(1, report["routing_sources"]["candidate_sources"]) - self.assertEqual(0, report["routing_sources"]["route_carriers"]) - self.assertEqual({"RoutingRange": 1}, report["routing_sources"]["marked_source_counts"]) - self.assertIn("已识别到布线源 1 个,但还没有生成可用路径 carrier", message) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) - def test_network_entry_uses_terminal_access_max_distance_when_smaller(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(500, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], - project_uuid="project-1", - kind="WireDuct", + self.assertEqual(1, report["routed"]) + self.assertEqual(1, diagnostic_payload["route_quality_warning_count"]) + self.assertEqual( + "wire-surface", + diagnostic_payload["route_quality_warning_samples"][0]["wire_uuid"], ) - - route = auto_routing.build_network_route( - start, - end, - options={"terminal_access_max_distance": 30.0}, - doc=doc, + self.assertEqual( + ["RoutingRange"], + diagnostic_payload["route_quality_warning_samples"][0]["carrier_kinds"], ) - self.assertIsNone(route) - - def test_route_eplan_connections_writes_diagnostic_object_for_missing_terminal(self): + def test_compact_batch_report_includes_entry_distance_warning_samples(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - payload = { - "project_uuid": "project-1", - "wires": [ + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "terminal_access_warning_distance": 100.0, + "routes": [ { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-missing", + "wire_uuid": "wire-long-entry", + "wire_label": "N-LONG", + "wire_object_label": "N-LONG: T1 -> T2 (Routed)", + "network": { + "entry_distance": 125.0, + "exit_distance": 20.0, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "label": "主线槽A"}}, + ], + }, } ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - - self.assertEqual(1, report["skipped_missing_terminal"]) - self.assertIsNotNone(diagnostic_group) - self.assertEqual(1, len(diagnostic_group.Group)) - diagnostic = diagnostic_group.Group[0] - self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) - self.assertEqual("project-1", diagnostic.QetProjectUuid) - self.assertFalse(diagnostic.QetDiagnosticOk) - self.assertIn("missing_terminals", diagnostic.QetDiagnosticIssueCodes) - self.assertIn("端子匹配失败", diagnostic.QetDiagnosticIssueLabels) - self.assertIn("批量生成布线连接完成", diagnostic.QetDiagnosticMessage) - self.assertIn("缺失端子 1 条", diagnostic.QetDiagnosticMessage) - self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) - - def test_route_eplan_connections_writes_diagnostic_object_when_no_wire_tasks(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - payload = {"project_uuid": "project-1", "wires": []} - - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - - self.assertEqual(0, report["total_wires"]) - self.assertIn("no_wire_tasks", report["issue_codes"]) - self.assertIsNotNone(diagnostic_group) - self.assertEqual(1, len(diagnostic_group.Group)) - diagnostic = diagnostic_group.Group[0] - self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) - self.assertEqual("project-1", diagnostic.QetProjectUuid) - self.assertFalse(diagnostic.QetDiagnosticOk) - self.assertIn("routed=0", diagnostic.QetDiagnosticMessage) - self.assertIn("没有导线任务", diagnostic.QetDiagnosticMessage) - diagnostic_payload = json.loads(diagnostic.QetDiagnosticJson) - self.assertEqual(0, diagnostic_payload["total_wires"]) - self.assertIn("no_wire_tasks", diagnostic_payload["issue_codes"]) + payload = auto_routing._compact_routing_connection_batch_report(report) - def test_route_eplan_connections_writes_compact_batch_diagnostic(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 20, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 20, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", + self.assertEqual(1, payload["route_entry_distance_warning_count"]) + self.assertEqual( + "wire-long-entry", + payload["route_entry_distance_warning_samples"][0]["wire_uuid"], ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], - project_uuid="project-1", - kind="WireDuct", + self.assertEqual( + "N-LONG: T1 -> T2 (Routed)", + payload["route_entry_distance_warning_samples"][0]["wire_object_label"], ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "wire_label": "N1", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, + self.assertEqual( + ["entry"], + payload["route_entry_distance_warning_samples"][0]["warning_sides"], + ) + self.assertEqual( + ["主线槽A"], + payload["route_entry_distance_warning_samples"][0]["route_source_labels"], + ) + + def test_compact_batch_report_quality_warning_includes_specific_carrier_labels(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ { - "wire_id": "wire-b", - "wire_label": "N2", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, + "wire_uuid": "wire-surface", + "wire_label": "N-SURFACE", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "RoutingRange", + "label": "安装板辅助路径", + } + } + ], + }, + } ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, report["runtime_version"]) - self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, diagnostic_payload["runtime_version"]) - self.assertEqual(2, len(report["routes"])) - self.assertNotIn("routes", diagnostic_payload) - self.assertEqual(2, diagnostic_payload["route_sample_count"]) - self.assertEqual(2, len(diagnostic_payload["route_samples"])) - self.assertEqual("wire-a", diagnostic_payload["route_samples"][0]["wire_uuid"]) - self.assertEqual("Routed", diagnostic_payload["route_samples"][0]["route_status"]) + self.assertEqual( + ["安装板辅助路径"], + payload["route_quality_warning_samples"][0]["route_carrier_labels"], + ) - def test_compact_batch_report_prioritizes_problem_route_samples(self): + def test_compact_batch_report_includes_candidate_obstacle_warning_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "total_wires": 3, - "routed": 3, + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, "routes": [ - {"wire_uuid": "normal-a", "route_status": "Routed"}, - {"wire_uuid": "normal-b", "route_status": "Routed"}, { - "wire_uuid": "problem-collision", - "route_status": "CollisionWarning", - "collisions": [ - { - "collision_kind": "HardIntersection", - "collision_relation": "third_party_device_collision", - } - ], - }, + "wire_uuid": "wire-obstacle-entry", + "wire_label": "N-OBSTACLE", + "network": { + "route_candidate_obstacle_hits": 2, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "UserPath", "label": "绕行路径A"}}, + ], + }, + } ], } - payload = auto_routing._compact_routing_connection_batch_report( - report, - sample_limit=2, - ) + payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual(3, payload["route_count"]) - self.assertEqual(2, payload["route_sample_count"]) - self.assertEqual("problem-collision", payload["route_samples"][0]["wire_uuid"]) + self.assertEqual(1, payload["route_candidate_obstacle_warning_count"]) self.assertEqual( - ["collision_warnings", "third_party_device_collisions"], - payload["route_samples"][0]["issue_codes"], + "wire-obstacle-entry", + payload["route_candidate_obstacle_warning_samples"][0]["wire_uuid"], + ) + self.assertEqual(2, payload["route_candidate_obstacle_warning_samples"][0]["hits"]) + self.assertEqual( + ["绕行路径A"], + payload["route_candidate_obstacle_warning_samples"][0]["route_source_labels"], ) - def test_compact_route_sample_includes_wire_object_label(self): + def test_compact_batch_report_includes_candidate_boundary_warning_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-label", - "wire_label": "N4111", - "wire_object_label": "N4111: terminal-start -> terminal-end (Routed)", - } - ) + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_uuid": "wire-outside-cabinet", + "wire_label": "N-OUT", + "network": { + "boundary_aware": True, + "route_candidate_boundary_violations": 3, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "UserPath", "label": "柜内主路径A"}}, + ], + }, + } + ], + } + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual(1, payload["route_candidate_boundary_warning_count"]) self.assertEqual( - "N4111: terminal-start -> terminal-end (Routed)", - sample["wire_object_label"], + "wire-outside-cabinet", + payload["route_candidate_boundary_warning_samples"][0]["wire_uuid"], + ) + self.assertEqual( + 3, + payload["route_candidate_boundary_warning_samples"][0]["violations"], + ) + self.assertEqual( + ["柜内主路径A"], + payload["route_candidate_boundary_warning_samples"][0]["route_source_labels"], ) - def test_compact_route_sample_prefers_route_track_bridged_segment_count(self): + def test_compact_batch_report_includes_route_constraint_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-bridge", - "route_track": { - "bridged_segments": 1, - }, - "network": { - "bridged_segments": 3, - }, - } - ) + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_uuid": "wire-constrained", + "wire_label": "N-CONSTRAINT", + "network": { + "route_constraints": { + "required": {"labels": ["必经路径"]}, + "forbidden": {"labels": ["禁止路径"]}, + }, + }, + } + ], + } - self.assertEqual(1, sample["network"]["bridged_segments"]) + payload = auto_routing._compact_routing_connection_batch_report(report) - def test_compact_route_sample_includes_candidate_obstacle_hits(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-obstacle-entry", - "network": { - "entry_candidate_rank": 3, - "exit_candidate_rank": 2, - "entry_candidate_score": 125.0, - "route_candidate_obstacle_hits": 2, - }, - } + self.assertEqual(1, payload["route_constraint_warning_count"]) + self.assertEqual( + "wire-constrained", + payload["route_constraint_warning_samples"][0]["wire_uuid"], + ) + self.assertEqual( + ["必经路径"], + payload["route_constraint_warning_samples"][0]["required"]["labels"], + ) + self.assertEqual( + ["禁止路径"], + payload["route_constraint_warning_samples"][0]["forbidden"]["labels"], ) - self.assertEqual(3, sample["network"]["entry_candidate_rank"]) - self.assertEqual(2, sample["network"]["exit_candidate_rank"]) - self.assertEqual(125.0, sample["network"]["entry_candidate_score"]) - self.assertEqual(2, sample["network"]["route_candidate_obstacle_hits"]) - - def test_compact_route_sample_includes_candidate_boundary_metadata(self): + def test_compact_batch_report_includes_capacity_pressure_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-boundary", - "network": { - "boundary_aware": True, - "route_candidate_boundary_violations": 2, - }, - } - ) + report = { + "routed": 3, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_uuid": "wire-crowded", + "wire_label": "N-CROWDED", + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "name": "DuctA", "capacity": 2}}, + {"carrier": {"kind": "WireDuct", "name": "DuctB", "capacity": 4}}, + ] + }, + } + ], + } - self.assertTrue(sample["network"]["boundary_aware"]) - self.assertEqual(2, sample["network"]["route_candidate_boundary_violations"]) + payload = auto_routing._compact_routing_connection_batch_report(report) - def test_compact_route_sample_includes_single_wire_status_summaries(self): - _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-status-summary", - "collisions": [ - {"collision_kind": "HardIntersection"}, - {"collision_kind": "ClearanceWarning"}, - ], - "lane": {"index": 2, "spacing_mm": 10.0, "axis": "y"}, - "network": { - "entry_distance": 125.0, - "exit_distance": 20.0, - "terminal_access_warning_distance": 100.0, - "boundary_aware": True, - "route_candidate_boundary_violations": 1, - }, - "route_track": { - "segments": [ - { - "carrier": { - "kind": "RoutingRange", - "label": "安装板兜底路径", - "capacity": 1, - } - } - ] - }, - } + self.assertEqual(1, payload["route_capacity_pressure_warning_count"]) + self.assertEqual( + "wire-crowded", + payload["route_capacity_pressure_warning_samples"][0]["wire_uuid"], + ) + self.assertEqual( + 3, + payload["route_capacity_pressure_warning_samples"][0]["max_parallel_wires"], + ) + self.assertEqual( + 2, + payload["route_capacity_pressure_warning_samples"][0]["min_capacity"], + ) + self.assertEqual( + ["DuctA", "DuctB"], + payload["route_capacity_pressure_warning_samples"][0]["carrier_names"], ) - - self.assertEqual("LongAccessWarning", sample["access"]["access_status"]) - self.assertEqual(["entry"], sample["access"]["warning_sides"]) - self.assertEqual("HardIntersectionWarning", sample["collision_summary"]["collision_status"]) - self.assertEqual(1, sample["collision_summary"]["hard_intersection_count"]) - self.assertEqual(1, sample["collision_summary"]["clearance_warning_count"]) - self.assertEqual("FallbackPathWarning", sample["quality"]["quality_status"]) - self.assertEqual(["RoutingRange"], sample["quality"]["fallback_carrier_kinds"]) - self.assertEqual("CapacityWarning", sample["capacity"]["capacity_status"]) - self.assertEqual(3, sample["capacity"]["parallel_wire_count"]) - self.assertEqual("BoundaryWarning", sample["boundary"]["boundary_status"]) self.assertEqual( - [ - "long_terminal_access", - "collision_warnings", - "route_quality_warnings", - "route_capacity_pressure", - "route_candidate_boundary_violations", - ], - sample["issue_codes"], + ["DuctA"], + payload["route_capacity_pressure_warning_samples"][0]["bottleneck_carrier_names"], + ) + self.assertEqual( + ["WireDuct"], + payload["route_capacity_pressure_warning_samples"][0]["bottleneck_carrier_kinds"], ) - self.assertIn("端子接入过长", sample["issue_labels"]) - self.assertIn("碰撞告警", sample["issue_labels"]) - def test_compact_route_sample_includes_route_constraints(self): + def test_compact_batch_report_capacity_pressure_includes_user_path_source_labels(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-constraints", - "network": { - "route_constraints": { - "required": {"labels": ["必经路径"]}, - "forbidden": {"labels": ["禁止路径"]}, + report = { + "routed": 3, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_uuid": "wire-crowded", + "wire_label": "N-CROWDED", + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + { + "carrier": { + "kind": "UserPath", + "name": "QETRoutePath_001", + "capacity": 1, + "source_label": "黄色主路径", + "source_path_index": "1", + } + } + ] }, - }, - } - ) + } + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( - ["必经路径"], - sample["network"]["route_constraints"]["required"]["labels"], - ) - self.assertEqual( - ["禁止路径"], - sample["network"]["route_constraints"]["forbidden"]["labels"], + ["黄色主路径(路径1)"], + payload["route_capacity_pressure_warning_samples"][0]["route_source_labels"], ) - def test_compact_route_sample_formats_user_path_source_index(self): + def test_compact_batch_report_includes_collision_kind_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-source-index", - "route_track": { - "segments": [ + report = { + "total_wires": 2, + "routed": 2, + "collision_warnings": 2, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_uuid": "wire-hard", + "collision_samples": [ + {"collision_kind": "HardIntersection", "obstacle_label": "设备A"}, + ], + }, + { + "wire_uuid": "wire-clearance", + "collision_samples": [ + {"collision_kind": "ClearanceWarning", "obstacle_label": "设备B"}, + ], + }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual(1, payload["collision_kind_counts"]["HardIntersection"]) + self.assertEqual(1, payload["collision_kind_counts"]["ClearanceWarning"]) + + def test_compact_batch_report_includes_collision_relation_counts(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 2, + "routed": 2, + "collision_warnings": 2, + "skipped_missing_terminal": 0, + "selective_collision_reroute": True, + "selective_collision_reroute_limit": 5, + "selective_collision_reroute_attempts": 2, + "selective_collision_reroutes": 1, + "selective_collision_reroute_no_improvement": 1, + "selective_collision_reroute_rejected_fallback": 1, + "selective_collision_reroute_errors": 0, + "routes": [ + { + "collision_samples": [ { - "carrier": { - "kind": "UserPath", - "source_label": "多路径草图", - "source_path_index": "1", - } + "collision_kind": "HardIntersection", + "collision_relation": "third_party_device_collision", + "obstacle_label": "设备A", }, + ], + }, + { + "collision_samples": [ { - "carrier": { - "kind": "UserPath", - "source_label": "多路径草图", - "source_path_index": "2", - } + "collision_kind": "ClearanceWarning", + "collision_relation": "endpoint_device_collision", + "obstacle_label": "设备B", }, - ] + ], }, - } - ) + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + self.assertEqual(1, payload["collision_relation_counts"]["third_party_device_collision"]) + self.assertEqual(1, payload["collision_relation_counts"]["endpoint_device_collision"]) + self.assertEqual(2, payload["selective_collision_reroute_attempts"]) + self.assertEqual(1, payload["selective_collision_reroutes"]) + self.assertEqual(1, payload["selective_collision_reroute_no_improvement"]) + self.assertEqual(1, payload["selective_collision_reroute_rejected_fallback"]) + self.assertIn("third_party_device_collisions", payload["issue_codes"]) + self.assertIn("endpoint_device_collisions", payload["issue_codes"]) + self.assertIn("main_path_detour_missing", payload["issue_codes"]) self.assertEqual( - ["多路径草图(路径1)", "多路径草图(路径2)"], - sample["route_source_labels"], + "selective_local_reroute_or_user_path", + payload["collision_reroute_recommendation"]["strategy"], ) + self.assertFalse(payload["collision_reroute_recommendation"]["global_avoid_obstacles_recommended"]) + self.assertIn("局部", payload["collision_reroute_recommendation"]["reason"]) - def test_compact_route_sample_ignores_bridge_only_carrier_summary(self): + def test_compact_batch_report_includes_top_collision_obstacles(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-bridge", - "route_track": { - "carrier_kinds": {"RoutingRange": 1}, - "carrier_names": ["VirtualBridge"], - "segments": [ + report = { + "total_wires": 3, + "routed": 3, + "collision_warnings": 3, + "skipped_missing_terminal": 0, + "routes": [ + { + "collision_samples": [ { - "is_bridge": True, - "carrier": {"name": "VirtualBridge", "kind": "RoutingRange"}, + "collision_kind": "HardIntersection", + "obstacle_name": "DeviceAObject", + "obstacle_label": "设备A", + "obstacle_parent_labels": ["安装板A"], + "obstacle_parent_names": ["MountPanelA"], }, { - "carrier": {"name": "WireDuctA", "kind": "WireDuct"}, + "collision_kind": "ClearanceWarning", + "obstacle_name": "DeviceAObject", + "obstacle_label": "设备A", + "obstacle_parent_labels": ["安装板A"], + "obstacle_parent_names": ["MountPanelA"], }, ], }, - } - ) - - self.assertEqual({"WireDuct": 1}, sample["carrier_kinds"]) - self.assertEqual(["WireDuctA"], sample["carrier_names"]) - - def test_route_eplan_connections_batch_diagnostic_includes_quality_warnings(self): - _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="RoutingRange", - ) - payload = { - "project_uuid": "project-1", - "wires": [ { - "wire_id": "wire-surface", - "wire_label": "N-SURFACE", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } + "collision_samples": [ + { + "collision_kind": "HardIntersection", + "obstacle_name": "BracketBObject", + "obstacle_label": "支架B", + "obstacle_parent_labels": ["柜体总成"], + "obstacle_parent_names": ["CabinetAssembly"], + }, + ], + }, ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") - diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual(1, report["routed"]) - self.assertEqual(1, diagnostic_payload["route_quality_warning_count"]) - self.assertEqual( - "wire-surface", - diagnostic_payload["route_quality_warning_samples"][0]["wire_uuid"], - ) self.assertEqual( - ["RoutingRange"], - diagnostic_payload["route_quality_warning_samples"][0]["carrier_kinds"], + [ + { + "label": "设备A", + "name": "DeviceAObject", + "count": 2, + "collision_kind_counts": { + "HardIntersection": 1, + "ClearanceWarning": 1, + }, + "parent_labels": ["安装板A"], + "parent_names": ["MountPanelA"], + "resolution_hint_code": "review_device_or_layout_collision", + "resolution_hint_label": "疑似设备/安装区域碰撞,优先补柜内路径或调整装配", + }, + { + "label": "支架B", + "name": "BracketBObject", + "count": 1, + "collision_kind_counts": {"HardIntersection": 1}, + "parent_labels": ["柜体总成"], + "parent_names": ["CabinetAssembly"], + "resolution_hint_code": "review_pass_through_structural_obstacle", + "resolution_hint_label": "疑似柜体/门板/支架结构,确认可穿越后标记忽略碰撞", + }, + ], + payload["top_collision_obstacles"], ) - def test_compact_batch_report_includes_entry_distance_warning_samples(self): + def test_compact_batch_report_summarizes_collision_resolution_categories(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, - "collision_warnings": 0, + "routed": 2, + "collision_warnings": 2, "skipped_missing_terminal": 0, - "terminal_access_warning_distance": 100.0, "routes": [ { - "wire_uuid": "wire-long-entry", - "wire_label": "N-LONG", - "wire_object_label": "N-LONG: T1 -> T2 (Routed)", - "network": { - "entry_distance": 125.0, - "exit_distance": 20.0, - }, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "label": "主线槽A"}}, - ], - }, - } + "wire_uuid": "wire-device", + "collision_samples": [ + { + "obstacle_label": "ID:12", + "obstacle_name": "QETDevice_A", + "collision_kind": "HardIntersection", + "obstacle_parent_labels": ["QET Exchange Devices"], + } + ], + }, + { + "wire_uuid": "wire-structure", + "collision_samples": [ + { + "obstacle_label": "NAUO141", + "obstacle_name": "Compound039", + "collision_kind": "HardIntersection", + "obstacle_parent_labels": ["FRONT DOOR-R ASS'Y"], + "obstacle_parent_names": ["DoorAssembly"], + }, + { + "obstacle_label": "支架B", + "obstacle_name": "BracketB", + "collision_kind": "ClearanceWarning", + }, + ], + }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual(1, payload["route_entry_distance_warning_count"]) - self.assertEqual( - "wire-long-entry", - payload["route_entry_distance_warning_samples"][0]["wire_uuid"], - ) self.assertEqual( - "N-LONG: T1 -> T2 (Routed)", - payload["route_entry_distance_warning_samples"][0]["wire_object_label"], - ) - self.assertEqual( - ["entry"], - payload["route_entry_distance_warning_samples"][0]["warning_sides"], + { + "review_device_or_layout_collision": 1, + "review_pass_through_structural_obstacle": 2, + }, + payload["collision_resolution_summary"]["counts"], ) self.assertEqual( - ["主线槽A"], - payload["route_entry_distance_warning_samples"][0]["route_source_labels"], + "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;另有 1 个疑似设备/装配碰撞需要补路径或调整装配。", + payload["collision_resolution_summary"]["recommended_action"], ) - def test_compact_batch_report_quality_warning_includes_specific_carrier_labels(self): + def test_compact_batch_report_issue_codes_include_collision_resolution_categories(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, - "collision_warnings": 0, + "routed": 2, + "collision_warnings": 2, "skipped_missing_terminal": 0, "routes": [ { - "wire_uuid": "wire-surface", - "wire_label": "N-SURFACE", - "route_track": { - "segments": [ - { - "carrier": { - "kind": "RoutingRange", - "label": "安装板辅助路径", - } - } - ], - }, - } + "collision_samples": [ + { + "obstacle_label": "ID:12", + "obstacle_name": "QETDevice_A", + "collision_kind": "HardIntersection", + } + ], + }, + { + "collision_samples": [ + { + "obstacle_label": "NAUO141", + "obstacle_name": "Compound039", + "collision_kind": "HardIntersection", + "obstacle_parent_labels": ["FRONT DOOR-R ASS'Y"], + } + ], + }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual( - ["安装板辅助路径"], - payload["route_quality_warning_samples"][0]["route_carrier_labels"], - ) + self.assertIn("collision_warnings", payload["issue_codes"]) + self.assertIn("device_or_layout_collisions", payload["issue_codes"]) + self.assertIn("structural_collision_candidates", payload["issue_codes"]) + self.assertIn("设备/布局碰撞", payload["issue_labels"]) + self.assertIn("结构件碰撞候选", payload["issue_labels"]) - def test_compact_batch_report_includes_candidate_obstacle_warning_samples(self): + def test_compact_batch_report_issue_codes_include_missing_endpoint_reasons(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { + "total_wires": 3, "routed": 1, "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ + "skipped_missing_terminal": 2, + "missing_endpoint_uuids": ["terminal-missing-a", "terminal-missing-b"], + "missing_endpoint_samples": [ { - "wire_uuid": "wire-obstacle-entry", - "wire_label": "N-OBSTACLE", - "network": { - "route_candidate_obstacle_hits": 2, - }, - "route_track": { - "segments": [ - {"carrier": {"kind": "UserPath", "label": "绕行路径A"}}, - ], - }, - } + "wire_uuid": "wire-missing-device", + "wire_label": "N-MISSING", + "start_found": False, + "start_terminal_uuid": "terminal-missing-a", + "start_element_uuid": "device-a", + "start_terminal_display": "A1", + "start_missing_endpoint_reason_code": "device_not_in_3d_scene", + "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", + }, + { + "wire_uuid": "wire-mismatch", + "wire_label": "N-MISMATCH", + "end_found": False, + "end_terminal_uuid": "terminal-missing-b", + "end_element_uuid": "device-b", + "end_device_label": "设备B", + "end_terminal_display": "B1", + "end_missing_endpoint_reason_code": "terminal_uuid_not_in_element", + "end_missing_endpoint_reason_label": "同设备存在端子,但没有匹配该 terminal_uuid", + }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual(1, payload["route_candidate_obstacle_warning_count"]) - self.assertEqual( - "wire-obstacle-entry", - payload["route_candidate_obstacle_warning_samples"][0]["wire_uuid"], - ) - self.assertEqual(2, payload["route_candidate_obstacle_warning_samples"][0]["hits"]) + self.assertIn("missing_terminals", payload["issue_codes"]) + self.assertIn("device_not_in_3d_scene", payload["issue_codes"]) + self.assertIn("terminal_uuid_not_in_element", payload["issue_codes"]) + self.assertIn("3D场景缺少设备", payload["issue_labels"]) + self.assertIn("端子UUID不匹配", payload["issue_labels"]) self.assertEqual( - ["绕行路径A"], - payload["route_candidate_obstacle_warning_samples"][0]["route_source_labels"], + { + "device_not_in_3d_scene": 1, + "terminal_uuid_not_in_element": 1, + }, + payload["missing_terminal_summary"]["reason_code_counts"], ) + self.assertEqual(2, len(payload["missing_terminal_summary"]["device_groups"])) + self.assertEqual("device-a", payload["missing_terminal_summary"]["device_groups"][0]["element_uuid"]) + self.assertEqual("设备B", payload["missing_terminal_summary"]["device_groups"][1]["device_label"]) - def test_compact_batch_report_includes_candidate_boundary_warning_samples(self): + def test_routing_diagnostic_recommended_actions_use_collision_resolution_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 1, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ + summary = { + "issue_codes": ["collision_warnings"], + "batch_collision_resolution_summary": { + "counts": { + "review_pass_through_structural_obstacle": 2, + "review_device_or_layout_collision": 1, + }, + "recommended_action": ( + "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;" + "另有 1 个疑似设备/装配碰撞需要补路径或调整装配。" + ), + }, + "diagnostics": { + "RoutingConnectionBatch": { + "payload": {"collision_warnings": 3}, + } + }, + "routed_wire_issue_summary": {"issue_code_counts": {}}, + } + + actions = auto_routing._routing_diagnostic_recommended_actions(summary) + + self.assertIn("先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough", actions) + self.assertIn("另有 1 个疑似设备/装配碰撞需要补路径或调整装配", actions) + + def test_routing_diagnostic_recommended_actions_include_collision_object_and_wire_selection_with_parent_refs(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + summary = { + "issue_codes": ["collision_warnings"], + "batch_top_collision_obstacles": [ { - "wire_uuid": "wire-outside-cabinet", - "wire_label": "N-OUT", - "network": { - "boundary_aware": True, - "route_candidate_boundary_violations": 3, - }, - "route_track": { - "segments": [ - {"carrier": {"kind": "UserPath", "label": "柜内主路径A"}}, - ], - }, + "label": "N600", + "count": 3, + "parent_names": ["QETExchangeDevices"], + "parent_labels": ["QET Exchange Devices"], } ], + "diagnostics": { + "RoutingConnectionBatch": { + "payload": {"collision_warnings": 3}, + } + }, + "routed_wire_issue_summary": {"issue_code_counts": {}}, + } + + actions = auto_routing._routing_diagnostic_recommended_actions(summary) + + self.assertIn("点击“选择高发碰撞对象”和“选择碰撞导线”核对穿模位置", actions) + self.assertIn("点击“选择碰撞父装配”确认结构件后再标记忽略碰撞", actions) + + def test_routing_diagnostic_recommended_actions_distinguish_clearance_warnings(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + summary = { + "issue_codes": ["collision_warnings", "clearance_warnings"], + "diagnostics": { + "RoutingConnectionBatch": { + "payload": {"collision_warnings": 3}, + } + }, + "routed_wire_issue_summary": { + "issue_wire_count": 3, + "issue_code_counts": { + "collision_warnings": 3, + "clearance_warnings": 3, + }, + }, + } + + actions = auto_routing._routing_diagnostic_recommended_actions(summary) + + self.assertIn("间隙不足:核对设备安全间隙、线槽/UserPath位置,必要时补路径或调整装配", actions) + + def test_routing_diagnostic_recommended_actions_distinguish_hard_intersections(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + summary = { + "issue_codes": ["collision_warnings", "hard_intersections"], + "diagnostics": { + "RoutingConnectionBatch": { + "payload": {"collision_warnings": 1}, + } + }, + "routed_wire_issue_summary": { + "issue_wire_count": 1, + "issue_code_counts": { + "collision_warnings": 1, + "hard_intersections": 1, + }, + }, } - payload = auto_routing._compact_routing_connection_batch_report(report) + actions = auto_routing._routing_diagnostic_recommended_actions(summary) - self.assertEqual(1, payload["route_candidate_boundary_warning_count"]) - self.assertEqual( - "wire-outside-cabinet", - payload["route_candidate_boundary_warning_samples"][0]["wire_uuid"], - ) - self.assertEqual( - 3, - payload["route_candidate_boundary_warning_samples"][0]["violations"], - ) - self.assertEqual( - ["柜内主路径A"], - payload["route_candidate_boundary_warning_samples"][0]["route_source_labels"], - ) + self.assertIn("硬穿模:优先补 UserPath/线槽主路径或调整装配,不能直接忽略", actions) - def test_compact_batch_report_includes_route_constraint_samples(self): + def test_routing_diagnostic_recommended_actions_include_terminal_access_fallback_targets(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 1, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ - { - "wire_uuid": "wire-constrained", - "wire_label": "N-CONSTRAINT", - "network": { - "route_constraints": { - "required": {"labels": ["必经路径"]}, - "forbidden": {"labels": ["禁止路径"]}, - }, + summary = { + "issue_codes": ["terminal_access_fallback_targets"], + "diagnostics": { + "RoutingConnectionBatch": { + "payload": { + "terminal_access_fallback_target_samples": [ + { + "wire_label": "W1", + "endpoint": "start", + "target_label": "安装板布线面", + "target_distance": 35.0, + } + ] }, - } - ], + }, + }, + "routed_wire_issue_summary": {"issue_code_counts": {}}, } - payload = auto_routing._compact_routing_connection_batch_report(report) + actions = auto_routing._routing_diagnostic_recommended_actions(summary) - self.assertEqual(1, payload["route_constraint_warning_count"]) - self.assertEqual( - "wire-constrained", - payload["route_constraint_warning_samples"][0]["wire_uuid"], - ) - self.assertEqual( - ["必经路径"], - payload["route_constraint_warning_samples"][0]["required"]["labels"], - ) - self.assertEqual( - ["禁止路径"], - payload["route_constraint_warning_samples"][0]["forbidden"]["labels"], - ) + self.assertIn("优先补端子附近到线槽/UserPath 的接入桥,避免端子接入退回布线面", actions) + self.assertIn("点击“按诊断建议生成桥接”尝试自动补端子退回目标到最近主路径的 UserPath 桥", actions) + self.assertIn("按端子接入退回布线面示例定位设备侧缺口,再重新生成布线路径网络", actions) - def test_compact_batch_report_includes_capacity_pressure_samples(self): + def test_routing_diagnostic_recommended_actions_use_path_network_terminal_access_fallback_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 3, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ - { - "wire_uuid": "wire-crowded", - "wire_label": "N-CROWDED", - "lane": {"index": 2, "spacing_mm": 10.0}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "name": "DuctA", "capacity": 2}}, - {"carrier": {"kind": "WireDuct", "name": "DuctB", "capacity": 4}}, + summary = { + "issue_codes": ["terminal_access_fallback_targets"], + "diagnostics": { + "RoutingPathNetwork": { + "payload": { + "terminal_access_fallback_targets": [ + { + "target_label": "安装板布线面", + "terminal_label": "as", + "parent_device_label": "UD:8", + } ] }, - } + }, + }, + "routed_wire_issue_summary": {"issue_code_counts": {}}, + } + + actions = auto_routing._routing_diagnostic_recommended_actions(summary) + + self.assertIn("点击“按诊断建议生成桥接”尝试自动补端子退回目标到最近主路径的 UserPath 桥", actions) + self.assertIn("按端子接入退回布线面示例定位设备侧缺口,再重新生成布线路径网络", actions) + + def test_routing_diagnostic_recommended_actions_include_terminal_exit_issue_selection(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + summary = { + "issue_codes": [ + "terminal_exit_direction_corrected", + "terminal_exit_length_capped", + "invalid_terminal_exit_directions", + "invalid_terminal_local_routes", ], + "routed_wire_issue_summary": {"issue_code_counts": {}}, + "diagnostics": { + "RoutingPathNetwork": { + "payload": { + "corrected_terminal_exits": [{"name": "TerminalCorrectedExit"}], + "capped_terminal_exits": [{"name": "TerminalCappedExit"}], + "invalid_terminal_exit_directions": [{"name": "TerminalInvalidDirection"}], + "invalid_terminal_local_routes": [{"name": "TerminalInvalidLocalRoute"}], + }, + }, + }, } - payload = auto_routing._compact_routing_connection_batch_report(report) + actions = auto_routing._routing_diagnostic_recommended_actions(summary) - self.assertEqual(1, payload["route_capacity_pressure_warning_count"]) - self.assertEqual( - "wire-crowded", - payload["route_capacity_pressure_warning_samples"][0]["wire_uuid"], - ) - self.assertEqual( - 3, - payload["route_capacity_pressure_warning_samples"][0]["max_parallel_wires"], - ) - self.assertEqual( - 2, - payload["route_capacity_pressure_warning_samples"][0]["min_capacity"], - ) - self.assertEqual( - ["DuctA", "DuctB"], - payload["route_capacity_pressure_warning_samples"][0]["carrier_names"], - ) + self.assertIn("点击“选择出线问题端子”定位方向校正、长度截断、显式方向无效或局部路径无效的端子", actions) + self.assertIn("复查设备模板 CPoint/LCS 出线方向,必要时设置端子局部出线路径", actions) + self.assertIn("检查 QetTerminalExitDirectionJson,必要时用“选中端子设置出线方向”重写显式方向", actions) + self.assertIn("检查 QetTerminalLocalRoutePointsJson,必要时用“选中端子设置局部出线”重写局部路径", actions) - def test_compact_batch_report_capacity_pressure_includes_user_path_source_labels(self): + def test_routing_diagnostic_recommended_actions_include_unconnected_terminal_selection(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 3, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ - { - "wire_uuid": "wire-crowded", - "wire_label": "N-CROWDED", - "lane": {"index": 2, "spacing_mm": 10.0}, - "route_track": { - "segments": [ + summary = { + "issue_codes": ["unconnected_terminals"], + "routed_wire_issue_summary": {"issue_code_counts": {}}, + "diagnostics": { + "RoutingPathNetwork": { + "payload": { + "unconnected_terminals": [ { - "carrier": { - "kind": "UserPath", - "name": "QETRoutePath_001", - "capacity": 1, - "source_label": "黄色主路径", - "source_path_index": "1", - } + "name": "TerminalUnconnected", + "terminal_uuid": "terminal-unconnected", + "nearest_network_distance_mm": 125.0, } - ] + ], }, - } - ], + }, + }, } - payload = auto_routing._compact_routing_connection_batch_report(report) + actions = auto_routing._routing_diagnostic_recommended_actions(summary) - self.assertEqual( - ["黄色主路径(路径1)"], - payload["route_capacity_pressure_warning_samples"][0]["route_source_labels"], - ) + self.assertIn("点击“选择未接入端子”定位未接入路由网络或接入距离超限的端子", actions) + self.assertIn("补端子附近 UserPath/线槽入口,或确认设备装配位置和端子接入最大距离", actions) - def test_compact_batch_report_includes_collision_kind_counts(self): + def test_routing_diagnostic_recommended_actions_include_wire_outside_boundary_selection(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "total_wires": 2, - "routed": 2, - "collision_warnings": 2, - "skipped_missing_terminal": 0, - "routes": [ - { - "wire_uuid": "wire-hard", - "collision_samples": [ - {"collision_kind": "HardIntersection", "obstacle_label": "设备A"}, - ], - }, - { - "wire_uuid": "wire-clearance", - "collision_samples": [ - {"collision_kind": "ClearanceWarning", "obstacle_label": "设备B"}, - ], + summary = { + "issue_codes": ["route_candidate_boundary_violations"], + "routed_wire_issue_summary": { + "issue_wire_count": 1, + "issue_code_counts": {"route_candidate_boundary_violations": 1}, + }, + "diagnostics": { + "RoutingConnectionBatch": { + "payload": { + "wire_outside_boundary_count": 1, + }, }, - ], + }, } - payload = auto_routing._compact_routing_connection_batch_report(report) + actions = auto_routing._routing_diagnostic_recommended_actions(summary) - self.assertEqual(1, payload["collision_kind_counts"]["HardIntersection"]) - self.assertEqual(1, payload["collision_kind_counts"]["ClearanceWarning"]) + self.assertIn("点击“选择越界导线”定位越出柜内区域的导线及其路径", actions) - def test_compact_batch_report_includes_collision_relation_counts(self): + def test_compact_batch_report_includes_route_path_usage_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 2, - "collision_warnings": 2, + "collision_warnings": 0, "skipped_missing_terminal": 0, - "selective_collision_reroute": True, - "selective_collision_reroute_limit": 5, - "selective_collision_reroute_attempts": 2, - "selective_collision_reroutes": 1, - "selective_collision_reroute_no_improvement": 1, - "selective_collision_reroute_rejected_fallback": 1, - "selective_collision_reroute_errors": 0, "routes": [ { - "collision_samples": [ - { - "collision_kind": "HardIntersection", - "collision_relation": "third_party_device_collision", - "obstacle_label": "设备A", - }, - ], + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "label": "线槽A"}}, + ], + }, }, { - "collision_samples": [ - { - "collision_kind": "ClearanceWarning", - "collision_relation": "endpoint_device_collision", - "obstacle_label": "设备B", - }, - ], + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, + ], + }, }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual(1, payload["collision_relation_counts"]["third_party_device_collision"]) - self.assertEqual(1, payload["collision_relation_counts"]["endpoint_device_collision"]) - self.assertEqual(2, payload["selective_collision_reroute_attempts"]) - self.assertEqual(1, payload["selective_collision_reroutes"]) - self.assertEqual(1, payload["selective_collision_reroute_no_improvement"]) - self.assertEqual(1, payload["selective_collision_reroute_rejected_fallback"]) - self.assertIn("third_party_device_collisions", payload["issue_codes"]) - self.assertIn("endpoint_device_collisions", payload["issue_codes"]) - self.assertIn("main_path_detour_missing", payload["issue_codes"]) self.assertEqual( - "selective_local_reroute_or_user_path", - payload["collision_reroute_recommendation"]["strategy"], + {"main_path_routes": 1, "fallback_routes": 1}, + payload["route_path_usage"], ) - self.assertFalse(payload["collision_reroute_recommendation"]["global_avoid_obstacles_recommended"]) - self.assertIn("局部", payload["collision_reroute_recommendation"]["reason"]) - def test_compact_batch_report_includes_top_collision_obstacles(self): + def test_compact_batch_report_summarizes_terminal_access_consumption(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 3, "routed": 3, - "collision_warnings": 3, + "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { - "collision_samples": [ - { - "collision_kind": "HardIntersection", - "obstacle_name": "DeviceAObject", - "obstacle_label": "设备A", - "obstacle_parent_labels": ["安装板A"], - "obstacle_parent_names": ["MountPanelA"], - }, - { - "collision_kind": "ClearanceWarning", - "obstacle_name": "DeviceAObject", - "obstacle_label": "设备A", - "obstacle_parent_labels": ["安装板A"], - "obstacle_parent_names": ["MountPanelA"], - }, - ], + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + }, + "route_track": {"segments": []}, }, { - "collision_samples": [ - { - "collision_kind": "HardIntersection", - "obstacle_name": "BracketBObject", - "obstacle_label": "支架B", - "obstacle_parent_labels": ["柜体总成"], - "obstacle_parent_names": ["CabinetAssembly"], - }, - ], + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": False, + }, + "route_track": {"segments": []}, + }, + { + "network": { + "start_terminal_access_consumed": False, + "end_terminal_access_consumed": False, + }, + "route_track": {"segments": []}, }, ], } @@ -11145,206 +14122,278 @@ class AutoRoutingTest(unittest.TestCase): payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( - [ + { + "routes": 3, + "both_endpoints_consumed": 1, + "one_endpoint_consumed": 1, + "no_endpoint_consumed": 1, + "start_consumed": 2, + "end_consumed": 1, + }, + payload["terminal_access_usage"], + ) + self.assertIn( + "端子接入采用:两端接入 1 条,一端接入 1 条,未接入 1 条。", + auto_routing.format_eplan_connection_route_report(report), + ) + + def test_compact_batch_report_summarizes_terminal_access_target_kinds(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 2, + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ { - "label": "设备A", - "name": "DeviceAObject", - "count": 2, - "collision_kind_counts": { - "HardIntersection": 1, - "ClearanceWarning": 1, + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + "start_terminal_access_target_kind": "WireDuct", + "end_terminal_access_target_kind": "UserPath", }, - "parent_labels": ["安装板A"], - "parent_names": ["MountPanelA"], - "resolution_hint_code": "review_device_or_layout_collision", - "resolution_hint_label": "疑似设备/安装区域碰撞,优先补柜内路径或调整装配", + "route_track": {"segments": []}, }, { - "label": "支架B", - "name": "BracketBObject", - "count": 1, - "collision_kind_counts": {"HardIntersection": 1}, - "parent_labels": ["柜体总成"], - "parent_names": ["CabinetAssembly"], - "resolution_hint_code": "review_pass_through_structural_obstacle", - "resolution_hint_label": "疑似柜体/门板/支架结构,确认可穿越后标记忽略碰撞", + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + "start_terminal_access_target_kind": "RoutingRange", + "end_terminal_access_target_kind": "WireDuct", + }, + "route_track": {"segments": []}, }, ], - payload["top_collision_obstacles"], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual( + {"WireDuct": 2, "UserPath": 1, "RoutingRange": 1}, + payload["terminal_access_target_kind_counts"], + ) + self.assertIn( + "端子接入目标:WireDuct 2 个,UserPath 1 个,RoutingRange 1 个。", + auto_routing.format_eplan_connection_route_report(report), ) - def test_compact_batch_report_summarizes_collision_resolution_categories(self): + def test_compact_batch_report_flags_terminal_access_fallback_targets_when_main_path_exists(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 2, - "collision_warnings": 2, + "total_wires": 1, + "routed": 1, + "collision_warnings": 0, "skipped_missing_terminal": 0, + "route_network_carrier_kind_counts": { + "WireDuct": 2, + "UserPath": 1, + "RoutingRange": 4, + }, "routes": [ { - "wire_uuid": "wire-device", - "collision_samples": [ - { - "obstacle_label": "ID:12", - "obstacle_name": "QETDevice_A", - "collision_kind": "HardIntersection", - "obstacle_parent_labels": ["QET Exchange Devices"], - } - ], - }, - { - "wire_uuid": "wire-structure", - "collision_samples": [ - { - "obstacle_label": "NAUO141", - "obstacle_name": "Compound039", - "collision_kind": "HardIntersection", - "obstacle_parent_labels": ["FRONT DOOR-R ASS'Y"], - "obstacle_parent_names": ["DoorAssembly"], - }, - { - "obstacle_label": "支架B", - "obstacle_name": "BracketB", - "collision_kind": "ClearanceWarning", - }, - ], + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + "start_terminal_access_target_kind": "RoutingRange", + "start_terminal_access_target_name": "RoutingRange001", + "start_terminal_access_target_label": "安装板布线面", + "start_terminal_access_target_distance": 35.0, + "end_terminal_access_target_kind": "WireDuct", + }, + "route_track": {"segments": []}, }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) + self.assertIn("terminal_access_fallback_targets", payload["issue_codes"]) + self.assertIn("端子接入退回布线面", payload["issue_labels"]) + self.assertEqual(1, payload["terminal_access_fallback_target_count"]) self.assertEqual( { - "review_device_or_layout_collision": 1, - "review_pass_through_structural_obstacle": 2, + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "endpoint": "start", + "target_kind": "RoutingRange", + "target_name": "RoutingRange001", + "target_label": "安装板布线面", + "target_distance": 35.0, }, - payload["collision_resolution_summary"]["counts"], + payload["terminal_access_fallback_target_samples"][0], ) - self.assertEqual( - "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;另有 1 个疑似设备/装配碰撞需要补路径或调整装配。", - payload["collision_resolution_summary"]["recommended_action"], + self.assertIn( + "端子接入退回布线面:当前有线槽/UserPath/过线孔主路径 3 条,但仍有 1 个端子接入目标为 RoutingRange/辅助路径。示例 W1 起点接入到 安装板布线面,距离 35.0mm。", + auto_routing.format_eplan_connection_route_report(report), ) - def test_compact_batch_report_issue_codes_include_collision_resolution_categories(self): + def test_terminal_access_fallback_samples_include_endpoint_terminal_and_device_metadata(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 2, - "collision_warnings": 2, + "total_wires": 1, + "routed": 1, + "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { - "collision_samples": [ - { - "obstacle_label": "ID:12", - "obstacle_name": "QETDevice_A", - "collision_kind": "HardIntersection", - } - ], + "wire_uuid": "wire-fallback", + "wire_label": "W1", + "start_terminal_uuid": "terminal-ud8-as", + "start_terminal_name": "QETTerminal_UD8_as", + "start_terminal_label": "as", + "start_device_label": "UD:8", + "start_parent_device_name": "QETDevice_UD8", + "start_parent_device_label": "UD:8", + "end_terminal_uuid": "terminal-sa", + "network": { + "start_terminal_access_target_kind": "RoutingRange", + "start_terminal_access_target_name": "RoutingRange001", + "start_terminal_access_target_label": "安装板布线面", + }, + "route_track": {"segments": []}, }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + sample = payload["terminal_access_fallback_target_samples"][0] + + self.assertEqual("terminal-ud8-as", sample["terminal_uuid"]) + self.assertEqual("QETTerminal_UD8_as", sample["terminal_name"]) + self.assertEqual("as", sample["terminal_label"]) + self.assertEqual("QETDevice_UD8", sample["parent_device_name"]) + self.assertEqual("UD:8", sample["parent_device_label"]) + + def test_compact_batch_report_does_not_flag_terminal_access_fallback_targets_without_main_path(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 1, + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "route_network_carrier_kind_counts": { + "RoutingRange": 4, + }, + "routes": [ { - "collision_samples": [ - { - "obstacle_label": "NAUO141", - "obstacle_name": "Compound039", - "collision_kind": "HardIntersection", - "obstacle_parent_labels": ["FRONT DOOR-R ASS'Y"], - } - ], + "network": { + "start_terminal_access_consumed": True, + "end_terminal_access_consumed": True, + "start_terminal_access_target_kind": "RoutingRange", + "end_terminal_access_target_kind": "RoutingRange", + }, + "route_track": {"segments": []}, }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertIn("collision_warnings", payload["issue_codes"]) - self.assertIn("device_or_layout_collisions", payload["issue_codes"]) - self.assertIn("structural_collision_candidates", payload["issue_codes"]) - self.assertIn("设备/布局碰撞", payload["issue_labels"]) - self.assertIn("结构件碰撞候选", payload["issue_labels"]) + self.assertNotIn("terminal_access_fallback_targets", payload["issue_codes"]) - def test_compact_batch_report_issue_codes_include_missing_endpoint_reasons(self): + def test_compact_batch_report_flags_when_no_main_path_is_used(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "total_wires": 3, - "routed": 1, + "total_wires": 2, + "routed": 2, "collision_warnings": 0, - "skipped_missing_terminal": 2, - "missing_endpoint_uuids": ["terminal-missing-a", "terminal-missing-b"], - "missing_endpoint_samples": [ + "skipped_missing_terminal": 0, + "route_network_carrier_kind_counts": { + "WireDuct": 2, + "WireDuctOpenEnd": 4, + "RoutingRange": 10, + }, + "routes": [ { - "wire_uuid": "wire-missing-device", - "wire_label": "N-MISSING", - "start_found": False, - "start_terminal_uuid": "terminal-missing-a", - "start_element_uuid": "device-a", - "start_terminal_display": "A1", - "start_missing_endpoint_reason_code": "device_not_in_3d_scene", - "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, + ], + }, }, { - "wire_uuid": "wire-mismatch", - "wire_label": "N-MISMATCH", - "end_found": False, - "end_terminal_uuid": "terminal-missing-b", - "end_element_uuid": "device-b", - "end_device_label": "设备B", - "end_terminal_display": "B1", - "end_missing_endpoint_reason_code": "terminal_uuid_not_in_element", - "end_missing_endpoint_reason_label": "同设备存在端子,但没有匹配该 terminal_uuid", + "route_track": { + "segments": [ + {"carrier": {"kind": "AuxiliaryPath", "label": "门板辅助路径"}}, + ], + }, }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertIn("missing_terminals", payload["issue_codes"]) - self.assertIn("device_not_in_3d_scene", payload["issue_codes"]) - self.assertIn("terminal_uuid_not_in_element", payload["issue_codes"]) - self.assertIn("3D场景缺少设备", payload["issue_labels"]) - self.assertIn("端子UUID不匹配", payload["issue_labels"]) self.assertEqual( - { - "device_not_in_3d_scene": 1, - "terminal_uuid_not_in_element": 1, - }, - payload["missing_terminal_summary"]["reason_code_counts"], + {"main_path_routes": 0, "fallback_routes": 2}, + payload["route_path_usage"], + ) + self.assertEqual( + {"WireDuct": 2, "WireDuctOpenEnd": 4, "RoutingRange": 10}, + payload["route_network_carrier_kind_counts"], + ) + self.assertEqual(6, payload["route_network_main_path_carriers"]) + self.assertIn("main_path_not_used", payload["issue_codes"]) + self.assertIn("未使用线槽或用户主路径", payload["issue_labels"]) + self.assertIn( + "主路径未采用:当前有线槽/UserPath/过线孔路径 6 条,但本批次 2 条导线都走了布线面/辅助路径。", + auto_routing.format_eplan_connection_route_report(report), ) - self.assertEqual(2, len(payload["missing_terminal_summary"]["device_groups"])) - self.assertEqual("device-a", payload["missing_terminal_summary"]["device_groups"][0]["element_uuid"]) - self.assertEqual("设备B", payload["missing_terminal_summary"]["device_groups"][1]["device_label"]) - def test_routing_diagnostic_recommended_actions_use_collision_resolution_summary(self): + def test_compact_batch_report_flags_when_main_path_is_underused(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - summary = { - "issue_codes": ["collision_warnings"], - "batch_collision_resolution_summary": { - "counts": { - "review_pass_through_structural_obstacle": 2, - "review_device_or_layout_collision": 1, - }, - "recommended_action": ( - "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;" - "另有 1 个疑似设备/装配碰撞需要补路径或调整装配。" - ), - }, - "diagnostics": { - "RoutingConnectionBatch": { - "payload": {"collision_warnings": 3}, - } + report = { + "total_wires": 5, + "routed": 5, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "route_network_carrier_kind_counts": { + "WireDuct": 2, + "UserPath": 3, + "RoutingRange": 10, }, - "routed_wire_issue_summary": {"issue_code_counts": {}}, + "routes": [ + { + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "label": "主线槽A"}}, + ], + }, + }, + *[ + { + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "安装板布线面"}}, + ], + }, + } + for _index in range(4) + ], + ], } - actions = auto_routing._routing_diagnostic_recommended_actions(summary) + payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertIn("先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough", actions) - self.assertIn("另有 1 个疑似设备/装配碰撞需要补路径或调整装配", actions) + self.assertEqual( + {"main_path_routes": 1, "fallback_routes": 4}, + payload["route_path_usage"], + ) + self.assertIn("main_path_underused", payload["issue_codes"]) + self.assertIn("主路径使用率过低", payload["issue_labels"]) + self.assertIn( + "主路径使用率过低:当前有线槽/UserPath/过线孔路径 5 条,本批次 5 条导线中只有 1 条使用主路径,4 条仍走布线面/辅助路径。", + auto_routing.format_eplan_connection_route_report(report), + ) - def test_compact_batch_report_includes_route_path_usage_summary(self): + def test_compact_batch_report_does_not_flag_balanced_main_path_usage_as_underused(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { @@ -11352,6 +14401,10 @@ class AutoRoutingTest(unittest.TestCase): "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, + "route_network_carrier_kind_counts": { + "WireDuct": 1, + "RoutingRange": 1, + }, "routes": [ { "route_track": { @@ -11363,7 +14416,7 @@ class AutoRoutingTest(unittest.TestCase): { "route_track": { "segments": [ - {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, + {"carrier": {"kind": "RoutingRange", "label": "安装板布线面"}}, ], }, }, @@ -11372,36 +14425,30 @@ class AutoRoutingTest(unittest.TestCase): payload = auto_routing._compact_routing_connection_batch_report(report) - self.assertEqual( - {"main_path_routes": 1, "fallback_routes": 1}, - payload["route_path_usage"], - ) + self.assertNotIn("main_path_underused", payload["issue_codes"]) + self.assertFalse(payload["main_path_usage"]["underused"]) - def test_compact_batch_report_flags_when_no_main_path_is_used(self): + def test_compact_batch_report_exposes_wire_outside_boundary_aliases(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "total_wires": 2, - "routed": 2, + "total_wires": 1, + "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, - "route_network_carrier_kind_counts": { - "WireDuct": 2, - "WireDuctOpenEnd": 4, - "RoutingRange": 10, - }, "routes": [ { - "route_track": { - "segments": [ - {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, - ], + "wire_uuid": "wire-outside", + "wire_label": "N-OUT", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "network": { + "boundary_aware": True, + "route_candidate_boundary_violations": 3, }, - }, - { "route_track": { "segments": [ - {"carrier": {"kind": "AuxiliaryPath", "label": "门板辅助路径"}}, + {"carrier": {"kind": "UserPath", "label": "柜内主路径A"}}, ], }, }, @@ -11409,22 +14456,24 @@ class AutoRoutingTest(unittest.TestCase): } payload = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) + self.assertIn("route_candidate_boundary_violations", payload["issue_codes"]) + self.assertEqual(1, payload["wire_outside_boundary_count"]) self.assertEqual( - {"main_path_routes": 0, "fallback_routes": 2}, - payload["route_path_usage"], - ) - self.assertEqual( - {"WireDuct": 2, "WireDuctOpenEnd": 4, "RoutingRange": 10}, - payload["route_network_carrier_kind_counts"], - ) - self.assertEqual(6, payload["route_network_main_path_carriers"]) - self.assertIn("main_path_not_used", payload["issue_codes"]) - self.assertIn("未使用线槽或用户主路径", payload["issue_labels"]) - self.assertIn( - "主路径未采用:当前有线槽/UserPath/过线孔路径 6 条,但本批次 2 条导线都走了布线面/辅助路径。", - auto_routing.format_eplan_connection_route_report(report), + { + "wire_uuid": "wire-outside", + "wire_label": "N-OUT", + "wire_object_label": "", + "wire": "N-OUT", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "violations": 3, + "route_source_labels": ["柜内主路径A"], + }, + payload["wire_outside_boundary_samples"][0], ) + self.assertIn("导线越出柜内区域:1 条", message) def test_route_eplan_connections_report_includes_top_level_path_usage_summary(self): _install_fake_freecad() @@ -12223,6 +15272,31 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("示例导线 N-CROWDED", message) self.assertIn("DuctA", message) + def test_route_report_capacity_pressure_highlights_bottleneck_path(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 3, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_label": "N-CROWDED", + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "name": "DuctA", "capacity": 2}}, + {"carrier": {"kind": "WireDuct", "name": "DuctB", "capacity": 4}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("瓶颈路径 DuctA", message) + def test_route_report_capacity_pressure_prefers_user_path_source_label(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -13188,6 +16262,109 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("1", second_wire.QetRouteMinCarrierCapacity) self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + def test_route_eplan_connections_dense_shared_route_uses_secondary_lane_offset_after_primary_cap(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + wires = [] + for index in range(8): + suffix = str(index) + _terminal(doc, terminal_objects, "TerminalStart{0}".format(suffix), "terminal-start-{0}".format(suffix), app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd{0}".format(suffix), "terminal-end-{0}".format(suffix), app.Vector(100, 0, 0)) + wires.append( + { + "wire_id": "wire-{0}".format(index), + "start_terminal_uuid": "terminal-start-{0}".format(index), + "end_terminal_uuid": "terminal-end-{0}".format(index), + } + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + report = auto_routing.route_eplan_connections_from_payload( + doc, + {"project_uuid": "project-1", "wires": wires}, + options={"lane_spacing": 10.0, "lane_axis": "y", "lane_max_offset": 30.0}, + ) + + self.assertEqual(8, report["routed"]) + self.assertEqual(7, report["routes"][7]["lane"]["index"]) + self.assertEqual("y", report["routes"][7]["lane"]["axis"]) + self.assertEqual(30.0, report["routes"][7]["lane"]["offset_mm"]) + self.assertEqual("z", report["routes"][7]["lane"]["secondary_axis"]) + self.assertEqual(10.0, report["routes"][7]["lane"]["secondary_offset_mm"]) + routed_group = doc.getObject("QETWiring_04_Routed") + wire_by_uuid = { + getattr(wire, "QetWireUuid", ""): wire + for wire in list(getattr(routed_group, "Group", []) or []) + } + capped_primary_wire = wire_by_uuid["wire-5"] + secondary_offset_wire = wire_by_uuid["wire-7"] + capped_midpoints = [(round(point.y, 3), round(point.z, 3)) for point in capped_primary_wire.Points[1:-1]] + secondary_midpoints = [(round(point.y, 3), round(point.z, 3)) for point in secondary_offset_wire.Points[1:-1]] + self.assertNotEqual(capped_midpoints, secondary_midpoints) + self.assertTrue(any(abs(point.z - 30.0) <= 0.001 for point in secondary_offset_wire.Points[1:-1])) + + def test_route_eplan_connections_matches_duplicate_terminal_uuid_by_endpoint_device(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + + wrong_start = _terminal(doc, terminal_objects, "WrongStartP1", "P1", app.Vector(0, 0, 0)) + wrong_end = _terminal(doc, terminal_objects, "WrongEndP2", "P2", app.Vector(120, 0, 0)) + terminal_objects.set_terminal_semantics( + wrong_start, "project-1", "wrong-start", "P1", "instance-wrong-start", label="P1" + ) + terminal_objects.set_terminal_semantics( + wrong_end, "project-1", "wrong-end", "P2", "instance-wrong-end", label="P2" + ) + expected_start = _terminal(doc, terminal_objects, "ExpectedStartP1", "P1", app.Vector(0, 50, 0)) + expected_end = _terminal(doc, terminal_objects, "ExpectedEndP2", "P2", app.Vector(120, 50, 0)) + terminal_objects.set_terminal_semantics( + expected_start, "project-1", "device-start", "P1", "instance-device-start", label="P1" + ) + terminal_objects.set_terminal_semantics( + expected_end, "project-1", "device-end", "P2", "instance-device-end", label="P2" + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 50, 20), app.Vector(120, 50, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-target", + "start_element_uuid": "device-start", + "start_instance_id": "instance-device-start", + "start_terminal_uuid": "P1", + "end_element_uuid": "device-end", + "end_instance_id": "instance-device-end", + "end_terminal_uuid": "P2", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(1, report["routed"]) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = list(getattr(routed_group, "Group", []) or [])[0] + self.assertEqual("instance-device-start", wire.QetStartInstanceId) + self.assertEqual("instance-device-end", wire.QetEndInstanceId) + self.assertEqual(50.0, wire.Points[0].y) + self.assertEqual(50.0, wire.Points[-1].y) + def test_route_eplan_connections_lane_index_accounts_for_existing_routed_segments(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -13285,6 +16462,113 @@ class AutoRoutingTest(unittest.TestCase): self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + def test_route_eplan_connections_auto_lane_axis_avoids_cabinet_boundary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, 0, 100, -10, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] + ) + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 95, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 95, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 95, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 95, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 95, 20), app.Vector(100, 95, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0}, + ) + + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + self.assertEqual("z", report["routes"][1]["lane"]["axis"]) + self.assertEqual(0, report["routes"][1]["network"]["route_candidate_boundary_violations"]) + routed_group = doc.getObject("QETWiring_04_Routed") + second_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "wire-b" + ][0] + self.assertTrue(all(point.y <= 100.0 for point in second_wire.Points)) + self.assertTrue(any(abs(point.z - 30.0) <= 0.001 for point in second_wire.Points[1:-1])) + + def test_route_eplan_connections_auto_lane_axis_avoids_obstacle_side(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + obstacle = doc.addObject("Part::Feature", "ObstacleDevice") + obstacle.Label = "右侧设备障碍" + obstacle.Shape = FakeShape(FakeBoundBox(5, 15, 40, 60, 15, 25)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={ + "lane_spacing": 10.0, + "batch_avoid_obstacles": True, + "obstacle_clearance": 0.0, + }, + ) + + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + self.assertEqual("x", report["routes"][1]["lane"]["axis"]) + self.assertEqual(-10.0, report["routes"][1]["lane"]["offset_mm"]) + self.assertEqual("Routed", report["routes"][1]["route_status"]) + self.assertEqual(0, report["routes"][1]["network"]["route_candidate_obstacle_hits"]) + def test_route_eplan_connections_prefers_unused_alternate_route_segments(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -14999,6 +18283,7 @@ class AutoRoutingTest(unittest.TestCase): "点击“选择缺端子设备”定位需要补工程端子的设备", "点击“选择异常导线”定位带问题码的导线", "点击“选择长接入端子/设备”检查设备高度和局部出线路径", + "点击“选择高发碰撞对象”和“选择碰撞导线”核对穿模位置", "点击“选择碰撞父装配”确认结构件后再标记忽略碰撞", ], summary["recommended_actions"], @@ -15860,6 +19145,73 @@ class AutoRoutingTest(unittest.TestCase): self.assertFalse(indexed["qet-terminal-p1"].ViewObject.Visibility) self.assertFalse(indexed["qet-terminal-p2"].ViewObject.Visibility) + def test_bind_wire_task_terminals_keeps_duplicate_terminal_uuid_on_different_devices(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + for element_uuid, instance_id, x in ( + ("device-a", "instance-a", 0), + ("device-b", "instance-b", 100), + ): + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_{0}".format(element_uuid)) + root.addObject(device) + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", element_uuid) + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", instance_id) + terminal_objects.ensure_string_property(device, "QetProjectUuid", "QET Exchange", "", "project-1") + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id=instance_id, + ) + terminal = terminal_objects.create_lcs_object( + doc, + "QETTerminal_{0}_P1".format(instance_id), + placement=app.Placement(app.Vector(x, 0, 0), app.Rotation()), + label="P1", + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + element_uuid, + "local:{0}:P1".format(instance_id), + instance_id, + label="P1", + slot_name="P1", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_element_uuid": "device-a", + "start_instance_id": "instance-a", + "start_terminal_uuid": "same-terminal-uuid", + "start_terminal_display": "P1", + "end_element_uuid": "device-b", + "end_instance_id": "instance-b", + "end_terminal_uuid": "same-terminal-uuid", + "end_terminal_display": "P1", + } + ], + } + + report = auto_routing.bind_wire_task_terminals_from_payload(doc, payload) + + candidates = [ + terminal + for terminal in auto_routing._collect_routable_terminals(doc) + if getattr(terminal, "QetTerminalUuid", "") == "same-terminal-uuid" + ] + self.assertEqual(2, report["bound"]) + self.assertEqual( + ["instance-a", "instance-b"], + sorted(getattr(terminal, "QetInstanceId", "") for terminal in candidates), + ) + def test_route_eplan_connections_rebinds_local_template_terminals_from_wire_endpoints(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() diff --git a/tests/python/freecad_exchange_bootstrap_wiring_test.py b/tests/python/freecad_exchange_bootstrap_wiring_test.py index 502d785..47ffb6f 100644 --- a/tests/python/freecad_exchange_bootstrap_wiring_test.py +++ b/tests/python/freecad_exchange_bootstrap_wiring_test.py @@ -109,7 +109,7 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { - "schema_version": "1.2", + "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], @@ -166,20 +166,27 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): path.write_text(json.dumps(payload), encoding="utf-8") normalized = bootstrap.load_exchange_payload(str(path)) - self.assertEqual("device-inst-1", normalized["devices"][0]["instance_id"]) + self.assertEqual("device-inst-1", normalized["devices"][0]["device_instance_id"]) self.assertEqual("element-a", normalized["devices"][0]["element_uuid"]) self.assertEqual(["element-a"], normalized["devices"][0]["element_uuids"]) - self.assertEqual(1, len(normalized["terminals"])) - self.assertEqual("terminal-a", normalized["terminals"][0]["terminal_uuid"]) - self.assertEqual("device-inst-1", normalized["terminals"][0]["instance_id"]) - self.assertEqual("device-inst-1", normalized["device_models"][0]["instance_id"]) + self.assertNotIn("terminals", normalized) + self.assertEqual(1, len(normalized["devices"][0]["terminals"])) + self.assertEqual("terminal-a", normalized["devices"][0]["terminals"][0]["terminal_uuid"]) + self.assertEqual( + "device-inst-1", + normalized["devices"][0]["terminals"][0]["device_instance_id"], + ) + self.assertEqual( + "device-inst-1", + normalized["device_models"][0]["device_instance_id"], + ) def test_load_exchange_payload_preserves_wire_label_and_style_id(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { - "schema_version": "1.2", + "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], @@ -221,7 +228,7 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): with self.assertRaises(bootstrap.ExchangeValidationError): bootstrap.load_exchange_payload(str(path)) - def test_load_exchange_payload_detects_wire_properties_database_next_to_json(self): + def test_load_exchange_payload_rejects_non_v2_schema(self): _install_fake_modules() sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") @@ -233,6 +240,125 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): "wires": [], } + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "2d_to_3d.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaises(bootstrap.ExchangeValidationError): + bootstrap.load_exchange_payload(str(path)) + + def test_load_exchange_payload_rejects_legacy_device_instance_id_field(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "schema_version": "2.0", + "project_uuid": "project-1", + "devices": [ + { + "instance_id": "legacy-device-instance", + "display_tag": "QF1", + "terminals": [], + } + ], + "device_models": [], + "wires": [], + } + + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "2d_to_3d.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaises(bootstrap.ExchangeValidationError): + bootstrap.load_exchange_payload(str(path)) + + def test_load_exchange_payload_rejects_legacy_device_level_element_uuid(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "schema_version": "2.0", + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-inst-1", + "element_uuid": "legacy-device-element", + "display_tag": "QF1", + "terminals": [], + } + ], + "device_models": [], + "wires": [], + } + + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "2d_to_3d.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaises(bootstrap.ExchangeValidationError): + bootstrap.load_exchange_payload(str(path)) + + def test_load_exchange_payload_rejects_legacy_device_model_element_uuid(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "schema_version": "2.0", + "project_uuid": "project-1", + "devices": [], + "device_models": [ + { + "element_uuid": "legacy-device", + "resolved_model_path": r"D:\models\legacy.step", + } + ], + "wires": [], + } + + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "2d_to_3d.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaises(bootstrap.ExchangeValidationError): + bootstrap.load_exchange_payload(str(path)) + + def test_load_exchange_payload_ignores_legacy_wire_endpoint_instance_ids(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "schema_version": "2.0", + "project_uuid": "project-1", + "devices": [], + "device_models": [], + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-a", + "end_terminal_uuid": "terminal-b", + "start_instance_id": "legacy-start", + "end_instance_id": "legacy-end", + } + ], + } + + with tempfile.TemporaryDirectory() as temp_dir: + path = Path(temp_dir) / "2d_to_3d.json" + path.write_text(json.dumps(payload), encoding="utf-8") + normalized = bootstrap.load_exchange_payload(str(path)) + + self.assertEqual("wire-1", normalized["wires"][0]["wire_id"]) + self.assertNotIn("start_instance_id", normalized["wires"][0]) + self.assertNotIn("end_instance_id", normalized["wires"][0]) + + def test_load_exchange_payload_detects_wire_properties_database_next_to_json(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "schema_version": "2.0", + "project_uuid": "project-1", + "devices": [], + "device_models": [], + "wires": [], + } + with tempfile.TemporaryDirectory() as temp_dir: path = Path(temp_dir) / "2d_to_3d.json" db_path = Path(temp_dir) / "project-local.sqlite" @@ -261,7 +387,7 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): sys.modules.pop("ExchangeBootstrap", None) bootstrap = importlib.import_module("ExchangeBootstrap") payload = { - "schema_version": "1.2", + "schema_version": "2.0", "project_uuid": "project-1", "devices": [], "device_models": [], @@ -303,7 +429,6 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): payload = { "project_uuid": "project-1", "devices": [], - "terminals": [], "device_models": [], "wires": [], "wire_style_database_path": "D:/project/project-local.sqlite", diff --git a/tests/python/freecad_exchange_device_import_fcstd_test.py b/tests/python/freecad_exchange_device_import_fcstd_test.py index 30dcf48..8e57b1e 100644 --- a/tests/python/freecad_exchange_device_import_fcstd_test.py +++ b/tests/python/freecad_exchange_device_import_fcstd_test.py @@ -444,14 +444,19 @@ class FcstdDeviceImportTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-1", - "instance_id": "instance-1", + "device_instance_id": "instance-1", "display_tag": "QF1", + "terminals": [ + { + "terminal_uuid": "terminal-1", + "element_uuid": "device-1", + } + ], } ], "device_models": [ { - "element_uuid": "device-1", + "device_instance_id": "instance-1", "resolved_model_path": str(model_path), } ], @@ -779,7 +784,8 @@ class FcstdDeviceImportTest(unittest.TestCase): } ) - self.assertEqual(1, report["imported_devices"]) + self.assertEqual(0, report["imported_devices"]) + self.assertEqual(1, report["pending_devices"]) root = doc.getObject(device_import.ROOT_GROUP_NAME) self.assertIsNotNone(root) devices = [ @@ -790,6 +796,410 @@ class FcstdDeviceImportTest(unittest.TestCase): self.assertEqual(1, len(devices)) self.assertEqual("device-inst-1", devices[0].QetInstanceId) self.assertEqual("element-a", devices[0].QetElementUuid) + self.assertEqual("Pending", devices[0].QetAssemblyState) + + def test_import_devices_from_payload_registers_new_devices_as_pending_without_importing_model(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + doc.recompute = lambda: None + device_import._ensure_document = lambda scene_path: doc + import_calls = [] + device_import._import_model_into_group = lambda *args, **kwargs: import_calls.append((args, kwargs)) + + report = device_import.import_devices_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-inst-1", + "display_tag": "N600", + "terminals": [ + { + "terminal_uuid": "terminal-a", + "element_uuid": "element-a", + } + ], + } + ], + "device_models": [ + { + "device_instance_id": "device-inst-1", + "resolved_model_path": str(model_path), + } + ], + } + ) + + self.assertEqual([], import_calls) + self.assertEqual(0, report["imported_devices"]) + self.assertEqual(1, report["pending_devices"]) + root = doc.getObject(device_import.ROOT_GROUP_NAME) + devices = [ + obj + for obj in root.Group + if getattr(obj, "Name", "").startswith(device_import.DEVICE_GROUP_PREFIX) + ] + self.assertEqual(1, len(devices)) + self.assertEqual("Pending", devices[0].QetAssemblyState) + self.assertEqual(str(model_path), devices[0].QetResolvedModelPath) + self.assertEqual([], device_import._existing_model_objects(doc, devices[0])) + + def test_import_devices_from_payload_keeps_existing_pending_device_without_importing_model(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + doc.recompute = lambda: None + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root, + "element-a", + "device-inst-1", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + device_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + import_calls = [] + device_import._import_model_into_group = lambda *args, **kwargs: import_calls.append((args, kwargs)) + + report = device_import.import_devices_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-inst-1", + "display_tag": "N600", + "terminals": [ + { + "terminal_uuid": "terminal-a", + "element_uuid": "element-a", + } + ], + } + ], + "device_models": [ + { + "device_instance_id": "device-inst-1", + "resolved_model_path": str(model_path), + } + ], + } + ) + + self.assertEqual([], import_calls) + self.assertEqual(0, report["imported_devices"]) + self.assertEqual(1, report["pending_devices"]) + self.assertEqual("Pending", device_group.QetAssemblyState) + + def test_insert_pending_device_imports_model_and_marks_device_placed(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root, + "element-a", + "device-inst-1", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + device_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + import_calls = [] + + def fake_import_model(doc_arg, group_arg, path_arg, **kwargs): + import_calls.append((doc_arg, group_arg, path_arg, kwargs)) + body = doc_arg.addObject("Part::Feature", "DeviceBody") + group_arg.addObject(body) + return [body] + + device_import._import_model_into_group = fake_import_model + + result = device_import.insert_pending_device(doc, device_group) + + self.assertEqual(1, len(import_calls)) + self.assertEqual(str(model_path), import_calls[0][2]) + self.assertEqual(device_group, result["device"]) + self.assertEqual("Placed", device_group.QetAssemblyState) + self.assertEqual(["DeviceBody"], [obj.Name for obj in device_group.Group if obj.Name == "DeviceBody"]) + + def test_insert_pending_device_can_place_whole_device_on_mount_target(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + app = sys.modules["FreeCAD"] + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root, + "element-a", + "device-inst-1", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + device_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + mount_target = doc.addObject("App::Part", "MountingPlate") + mount_target.Label = "安装板" + mount_target.Placement = app.Placement(app.Vector(100, 200, 300), app.Rotation()) + device_import._ensure_string_property( + mount_target, + "QetCarrierKind", + "QET Mount", + "", + "mounting_plate", + ) + + def fake_import_model(doc_arg, group_arg, path_arg, **kwargs): + body = doc_arg.addObject("Part::Feature", "DeviceBody") + group_arg.addObject(body) + return [body] + + device_import._import_model_into_group = fake_import_model + + result = device_import.insert_pending_device( + doc, + device_group, + mount_target=mount_target, + ) + + self.assertEqual(device_group, result["device"]) + self.assertEqual("Placed", device_group.QetAssemblyState) + self.assertEqual(100.0, device_group.Placement.Base.x) + self.assertEqual(200.0, device_group.Placement.Base.y) + self.assertEqual(300.0, device_group.Placement.Base.z) + self.assertEqual("manual_insert", device_group.QetMountMode) + self.assertEqual("MountingPlate", device_group.QetMountHostName) + self.assertEqual("安装板", device_group.QetMountHostLabel) + self.assertEqual("mounting_plate", device_group.QetMountHostKind) + + def test_insert_pending_device_prefers_explicit_mount_placement_over_target_origin(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + app = sys.modules["FreeCAD"] + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root, + "element-a", + "device-inst-1", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + device_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + mount_target = doc.addObject("App::Part", "CabinetFace") + mount_target.Label = "柜体安装面" + mount_target.Placement = app.Placement(app.Vector(100, 200, 300), app.Rotation()) + explicit_placement = app.Placement(app.Vector(11, 22, 33), app.Rotation()) + + def fake_import_model(doc_arg, group_arg, path_arg, **kwargs): + body = doc_arg.addObject("Part::Feature", "DeviceBody") + group_arg.addObject(body) + return [body] + + device_import._import_model_into_group = fake_import_model + + device_import.insert_pending_device( + doc, + device_group, + mount_target=mount_target, + mount_placement=explicit_placement, + ) + + self.assertEqual(11.0, device_group.Placement.Base.x) + self.assertEqual(22.0, device_group.Placement.Base.y) + self.assertEqual(33.0, device_group.Placement.Base.z) + self.assertEqual("CabinetFace", device_group.QetMountHostName) + + def test_insert_pending_device_applies_mount_normal_offset_and_records_face_metadata(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + app = sys.modules["FreeCAD"] + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root, + "element-a", + "device-inst-1", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + device_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + mount_target = doc.addObject("App::Part", "CabinetFace") + mount_target.Label = "柜体安装面" + base_placement = app.Placement(app.Vector(10, 20, 30), app.Rotation()) + + def fake_import_model(doc_arg, group_arg, path_arg, **kwargs): + body = doc_arg.addObject("Part::Feature", "DeviceBody") + group_arg.addObject(body) + return [body] + + device_import._import_model_into_group = fake_import_model + + device_import.insert_pending_device( + doc, + device_group, + mount_target=mount_target, + mount_placement=base_placement, + mount_normal=app.Vector(0, 0, 1), + mount_offset_mm=5.0, + ) + + self.assertEqual(10.0, device_group.Placement.Base.x) + self.assertEqual(20.0, device_group.Placement.Base.y) + self.assertEqual(35.0, device_group.Placement.Base.z) + self.assertEqual("5.000000", device_group.QetMountOffsetMm) + self.assertIn('"z": 1.0', device_group.QetMountHostNormalJson) + + def test_register_commands_adds_insert_pending_device_command(self): + _install_fake_freecad(None) + gui = sys.modules["FreeCADGui"] + registered = {} + gui.addCommand = lambda name, command: registered.setdefault(name, command) + + device_import, _ = _reload_modules() + device_import.register_commands() + + self.assertIn("QET_Exchange_InsertPendingDevice", registered) + + def test_list_pending_devices_returns_device_groups_not_internal_model_children(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "jhd5.FCStd" + model_path.write_text("fake fcstd placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + pending_group, _ = device_import._ensure_device_group( + doc, + root, + "element-n600", + "inst-n600", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + pending_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + internal_model = doc.addObject("Part::Feature", "JHD5_6_grey001") + pending_group.addObject(internal_model) + + placed_group, _ = device_import._ensure_device_group( + doc, + root, + "element-ta", + "inst-ta", + str(model_path), + "TAa", + 1, + ) + device_import._set_device_assembly_state( + placed_group, + device_import.ASSEMBLY_STATE_PLACED, + ) + + pending_devices = device_import.list_pending_devices(doc) + + self.assertEqual(1, len(pending_devices)) + self.assertEqual("inst-n600", pending_devices[0]["instance_id"]) + self.assertEqual("N600", pending_devices[0]["display_tag"]) + self.assertEqual(str(model_path), pending_devices[0]["resolved_model_path"]) + self.assertIs(pending_group, pending_devices[0]["device"]) + + def test_pending_device_panel_registers_command_and_formats_pending_rows(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "jhd5.FCStd" + model_path.write_text("fake fcstd placeholder", encoding="utf-8") + _install_fake_freecad(None) + gui = sys.modules["FreeCADGui"] + registered = {} + gui.addCommand = lambda name, command: registered.setdefault(name, command) + + device_import, _ = _reload_modules() + sys.modules.pop("PendingDeviceAssemblyPanel", None) + pending_panel = importlib.import_module("PendingDeviceAssemblyPanel") + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + pending_group, _ = device_import._ensure_device_group( + doc, + root, + "element-n600", + "inst-n600", + str(model_path), + "N600", + 0, + ) + device_import._set_device_assembly_state( + pending_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + + rows = pending_panel.pending_device_rows(doc) + pending_panel.register_commands() + + self.assertEqual(1, len(rows)) + self.assertEqual("N600", rows[0]["display_tag"]) + self.assertIn("N600", rows[0]["display_text"]) + self.assertIn("jhd5.FCStd", rows[0]["display_text"]) + self.assertIn("QET_Exchange_OpenPendingDevicePanel", registered) def test_import_devices_from_payload_reuses_fcstd_source_document_within_one_sync(self): source = FakeDocument("TerminalSlice", r"D:\models\qet_terminal_slice.FCStd") @@ -841,7 +1251,8 @@ class FcstdDeviceImportTest(unittest.TestCase): "resolved_model_path": source.FileName, }, ], - } + }, + auto_insert_pending_devices=True, ) finally: device_import.os.path.isfile = original_isfile diff --git a/tests/python/freecad_exchange_terminal_import_template_slots_test.py b/tests/python/freecad_exchange_terminal_import_template_slots_test.py index 85055c9..92aa105 100644 --- a/tests/python/freecad_exchange_terminal_import_template_slots_test.py +++ b/tests/python/freecad_exchange_terminal_import_template_slots_test.py @@ -165,15 +165,13 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-a", - "instance_id": "instance-a", - } - ], - "terminals": [ - { - "terminal_uuid": "terminal-a", - "element_uuid": "device-a", - "instance_id": "instance-a", + "device_instance_id": "instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-a", + "element_uuid": "device-a", + } + ], } ], } @@ -253,11 +251,10 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-a", - "instance_id": "instance-a", + "device_instance_id": "instance-a", + "terminals": [], } ], - "terminals": [], } ) @@ -326,6 +323,33 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual(1, len(terminals)) self.assertEqual("terminal-a", terminals[0].QetTerminalUuid) + def test_import_rejects_legacy_top_level_terminals(self): + _install_fake_freecad() + terminal_import, _terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + + with self.assertRaises(terminal_import.TerminalImportError): + terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-a", + "terminals": [], + } + ], + "terminals": [ + { + "terminal_uuid": "terminal-a", + "element_uuid": "device-a", + "device_instance_id": "instance-a", + } + ], + } + ) + def test_import_synthesizes_missing_terminal_entries_from_wire_endpoints(self): _install_fake_freecad() terminal_import, terminal_objects, device_import = _reload_modules() @@ -367,19 +391,32 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): { "project_uuid": "project-1", "devices": [ - {"element_uuid": "device-a", "instance_id": "instance-a"}, - {"element_uuid": "device-b", "instance_id": "instance-b"}, + { + "device_instance_id": "instance-a", + "terminals": [ + { + "terminal_uuid": "known-a", + "element_uuid": "device-a", + } + ], + }, + { + "device_instance_id": "instance-b", + "terminals": [ + { + "terminal_uuid": "known-b", + "element_uuid": "device-b", + } + ], + }, ], - "terminals": [], "wires": [ { "wire_id": "wire-1", "start_element_uuid": "device-a", "start_terminal_uuid": "terminal-a", - "start_instance_id": "", "end_element_uuid": "device-b", "end_terminal_uuid": "terminal-b", - "end_instance_id": "", } ], } @@ -402,12 +439,12 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): ) ) - self.assertEqual(2, report["imported_terminals"]) + self.assertEqual(4, report["imported_terminals"]) self.assertEqual(2, report["synthesized_wire_endpoint_terminals"]) - self.assertEqual("terminal-a", start_terminals[0].QetTerminalUuid) - self.assertEqual("device-a", start_terminals[0].QetElementUuid) - self.assertEqual("terminal-b", end_terminals[0].QetTerminalUuid) - self.assertEqual("device-b", end_terminals[0].QetElementUuid) + start_by_uuid = {terminal.QetTerminalUuid: terminal for terminal in start_terminals} + end_by_uuid = {terminal.QetTerminalUuid: terminal for terminal in end_terminals} + self.assertEqual("device-a", start_by_uuid["terminal-a"].QetElementUuid) + self.assertEqual("device-b", end_by_uuid["terminal-b"].QetElementUuid) def test_import_reads_qet_terminals_embedded_in_devices(self): _install_fake_freecad() @@ -445,29 +482,24 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-a", - "instance_id": "instance-a", + "device_instance_id": "instance-a", "terminals": [ { "element_uuid": "device-a", - "instance_id": "instance-a", "terminal_uuid": "device-a:terminal-p1", "terminal_display": "P1", } ], } ], - "terminals": [], "wires": [ { "wire_id": "wire-1", "start_element_uuid": "device-a", "start_terminal_uuid": "device-a:terminal-p1", - "start_instance_id": "instance-a", "start_terminal_display": "P1", "end_element_uuid": "device-a", "end_terminal_uuid": "device-a:terminal-p1", - "end_instance_id": "instance-a", "end_terminal_display": "P1", } ], @@ -481,7 +513,7 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): terminals = terminal_objects.collect_terminal_objects(terminal_group) self.assertEqual(1, report["imported_terminals"]) - self.assertEqual(1, report["device_embedded_terminals"]) + self.assertEqual(0, report["device_embedded_terminals"]) self.assertEqual(0, report["synthesized_wire_endpoint_terminals"]) self.assertEqual(1, len(terminals)) self.assertEqual("device-a:terminal-p1", terminals[0].QetTerminalUuid) @@ -531,16 +563,20 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): { "project_uuid": "project-1", "devices": [ - {"element_uuid": "device-a", "instance_id": "instance-a"}, - {"element_uuid": "device-b", "instance_id": "instance-b"}, - ], - "terminals": [ { - "terminal_uuid": "terminal-a", - "element_uuid": "device-a", - "instance_id": "instance-b", - "terminal_display": "12", - } + "device_instance_id": "instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-a", + "element_uuid": "device-a", + "terminal_display": "12", + } + ], + }, + { + "device_instance_id": "instance-b", + "terminals": [], + }, ], } ) @@ -630,26 +666,23 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-a", - "instance_id": "instance-a", + "device_instance_id": "instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-p2", + "element_uuid": "device-a", + "slot_name_hint": "P2", + "terminal_label": "P2", + }, + { + "terminal_uuid": "terminal-p1", + "element_uuid": "device-a", + "slot_name_hint": "P1", + "terminal_label": "P1", + }, + ], } ], - "terminals": [ - { - "terminal_uuid": "terminal-p2", - "element_uuid": "device-a", - "instance_id": "instance-a", - "slot_name_hint": "P2", - "terminal_label": "P2", - }, - { - "terminal_uuid": "terminal-p1", - "element_uuid": "device-a", - "instance_id": "instance-a", - "slot_name_hint": "P1", - "terminal_label": "P1", - }, - ], } ) @@ -723,18 +756,16 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-a", - "instance_id": "instance-a", + "device_instance_id": "instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-p1", + "element_uuid": "device-a", + "terminal_display": "P1", + }, + ], } ], - "terminals": [ - { - "terminal_uuid": "terminal-p1", - "element_uuid": "device-a", - "instance_id": "instance-a", - "terminal_display": "P1", - }, - ], } ) @@ -822,16 +853,14 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): "project_uuid": "project-1", "devices": [ { - "element_uuid": "device-a", - "instance_id": "instance-a", - } - ], - "terminals": [ - { - "terminal_uuid": "terminal-real-p1", - "element_uuid": "device-a", - "instance_id": "instance-a", - "slot_name_hint": "P1", + "device_instance_id": "instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-real-p1", + "element_uuid": "device-a", + "slot_name_hint": "P1", + } + ], } ], } diff --git a/tests/python/freecad_exchange_wiring_import_test.py b/tests/python/freecad_exchange_wiring_import_test.py index 3b1980c..3718ff2 100644 --- a/tests/python/freecad_exchange_wiring_import_test.py +++ b/tests/python/freecad_exchange_wiring_import_test.py @@ -113,6 +113,28 @@ class WiringImportTest(unittest.TestCase): terminal_objects.ensure_root_group(doc, "project-1") payload = { "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-a", + "display_tag": "TAa", + "terminals": [ + { + "element_uuid": "device-a", + "terminal_uuid": "device-a:terminal-a", + } + ], + }, + { + "device_instance_id": "instance-b", + "display_tag": "PEN001", + "terminals": [ + { + "element_uuid": "device-b", + "terminal_uuid": "device-b:terminal-b", + } + ], + }, + ], "wires": [ { "wire_id": "wire-1", @@ -123,11 +145,9 @@ class WiringImportTest(unittest.TestCase): "net_uuid": "net-1", "group_uuid": "group-1", "start_element_uuid": "device-a", - "start_instance_id": "instance-a", "start_terminal_uuid": "device-a:terminal-a", "start_terminal_display": "A1", "end_element_uuid": "device-b", - "end_instance_id": "instance-b", "end_terminal_uuid": "device-b:terminal-b", "end_terminal_display": "B1", } @@ -153,8 +173,26 @@ class WiringImportTest(unittest.TestCase): payload = { "project_uuid": "project-1", "devices": [ - {"element_uuid": "device-a", "display_tag": "TAa"}, - {"element_uuid": "device-b", "display_tag": "PEN001"}, + { + "device_instance_id": "instance-a", + "display_tag": "TAa", + "terminals": [ + { + "element_uuid": "device-a", + "terminal_uuid": "device-a:terminal-a", + } + ], + }, + { + "device_instance_id": "instance-b", + "display_tag": "PEN001", + "terminals": [ + { + "element_uuid": "device-b", + "terminal_uuid": "device-b:terminal-b", + } + ], + }, ], "wires": [ { @@ -209,6 +247,50 @@ class WiringImportTest(unittest.TestCase): self.assertEqual("W001-updated", task_group.Group[0].QetWireMark) self.assertEqual("Routed", task_group.Group[0].RouteStatus) + def test_import_wire_tasks_maps_labels_from_nested_device_terminals(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, wiring_import = _reload_modules() + + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + payload = { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-qf1", + "display_tag": "QF1", + "terminals": [ + { + "element_uuid": "symbol-qf1-a", + "terminal_uuid": "terminal-a", + "terminal_display": "1", + }, + { + "element_uuid": "symbol-qf1-b", + "terminal_uuid": "terminal-b", + "terminal_display": "2", + }, + ], + } + ], + "wires": [ + { + "wire_id": "wire-1", + "start_element_uuid": "symbol-qf1-b", + "start_terminal_uuid": "terminal-b", + "start_terminal_display": "2", + "end_element_uuid": "symbol-qf1-a", + "end_terminal_uuid": "terminal-a", + "end_terminal_display": "1", + } + ], + } + + wiring_import.import_wire_tasks_from_payload(payload, doc) + + task = doc.getObject("QETWiring_01_Tasks").Group[0] + self.assertEqual("QF1:2 -> QF1:1", task.QetEndpointLabel) + def test_reimport_keeps_stale_wire_tasks_for_sync_marking(self): _install_fake_freecad() terminal_objects, wiring_objects, wiring_import = _reload_modules() diff --git a/tests/python/freecad_exchange_wiring_test.py b/tests/python/freecad_exchange_wiring_test.py index fa06c5d..5eddf26 100644 --- a/tests/python/freecad_exchange_wiring_test.py +++ b/tests/python/freecad_exchange_wiring_test.py @@ -158,7 +158,7 @@ class FakeDocument: def _reload_modules(): - for name in ["TerminalObjects", "WiringObjects", "ManualWiring", "ExchangeWriteBack"]: + for name in ["TerminalObjects", "WiringObjects", "ManualWiring", "ExchangeWriteBack", "DeviceImport"]: sys.modules.pop(name, None) import TerminalObjects import WiringObjects @@ -435,6 +435,81 @@ class WiringTest(unittest.TestCase): else: os.environ["QET_2D_TO_3D_JSON"] = old_json + def test_writeback_file_uses_v2_binding_field_names_only(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _manual_wiring, write_back = _reload_modules() + + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "instance-a", + ) + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="instance-a", + ) + terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_A") + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "device-a", + "terminal-a", + "terminal-instance-a", + label="A", + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + output_path = Path(tmp_dir) / "3d_to_2d.json" + report = write_back.write_back_document( + doc, + scene_path=str(Path(tmp_dir) / "scene.FCStd"), + payload={"project_uuid": "project-1"}, + ) + payload = json.loads(output_path.read_text(encoding="utf-8")) + + self.assertEqual(str(output_path), report["output_path"]) + self.assertEqual("2.0", payload["schema_version"]) + self.assertEqual( + [{"element_uuid": "device-a", "device_instance_id": "instance-a"}], + payload["instances"], + ) + self.assertEqual( + [ + { + "terminal_uuid": "terminal-a", + "device_instance_id": "instance-a", + "terminal_instance_id": "terminal-instance-a", + } + ], + payload["terminals"], + ) + def keys_from(value): + if isinstance(value, dict): + for key, child in value.items(): + yield key + yield from keys_from(child) + elif isinstance(value, list): + for child in value: + yield from keys_from(child) + + self.assertNotIn("instance_id", set(keys_from(payload))) + def test_writeback_skips_local_terminal_bindings(self): _install_fake_freecad() terminal_objects, _wiring_objects, _manual_wiring, write_back = _reload_modules() From c5147407cbb3212037bca4795288cd9c8d2b4e05 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Tue, 23 Jun 2026 10:23:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(freecad):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=AC=AC=E4=B8=80=E9=98=B6?= =?UTF-8?q?=E6=AE=B5=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 38 +- docs/FreeCAD 机柜装配操作文档.md | 86 +- src/Mod/FreeCADExchange/AutoRouting.py | 2423 ++++++++++++-- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 191 +- src/Mod/FreeCADExchange/DeviceImport.py | 14 +- src/Mod/FreeCADExchange/ExchangeWriteBack.py | 141 +- .../PendingDeviceAssemblyPanel.py | 318 +- src/Mod/FreeCADExchange/RoutingNetwork.py | 358 ++- src/Mod/FreeCADExchange/TerminalImport.py | 248 +- src/Mod/FreeCADExchange/TerminalObjects.py | 23 + .../freecad_exchange_auto_routing_test.py | 2841 ++++++++++++++++- ...eecad_exchange_device_import_fcstd_test.py | 212 ++ ...nge_terminal_import_template_slots_test.py | 488 +++ tests/python/freecad_exchange_wiring_test.py | 189 +- 14 files changed, 7118 insertions(+), 452 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index e754d98..d7b243d 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -147,6 +147,10 @@ terminal_uuid 点击自动布线面板中的 `检查布线准备度` 后,FreeCAD 还会在树目录 `QETWiring_05_Diagnostics` 下写入一个 `RoutingPreflight` 诊断对象。该对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示本次诊断是否通过;`QetDiagnosticIssueCodes` 直接列出问题码;`QetDiagnosticIssueLabels` 直接列出中文问题标签;`QetDiagnosticMessage` 保存中文摘要;`QetDiagnosticJson` 保存压缩后的最新预检结果,包括导线任务数量、工程端子数量、路径网络段数、布线源摘要、柜内边界摘要、导线样式库状态、问题码 `issue_codes`、缺失端点样例等。重复检查时旧的 `RoutingPreflight` 会被替换,只保留最新一次结果。 +如果当前 FreeCAD 文档里多个工程端子对象带有同一个 `QetTerminalUuid`,预检会追加 `duplicate_3d_terminal_uuids / 3D端子UUID重复`。这表示单靠 `terminal_uuid` 已经不足以唯一定位 3D 端子,后续匹配必须依赖导线端点里的 `element_uuid`、当前 3D 设备 `instance_id`、端子显示名/脚号等上下文。当前第一阶段会按这些上下文消歧;如果导线任务缺少设备上下文,可能会进入缺端点或端子 UUID 不匹配诊断,而不是随便连到第一个同 UUID 端子。 + +预检还会对 `devices[].terminals[]` 和 `wires[]` 做一致性检查。如果 JSON 中某个设备端子没有被任何导线端点引用,会追加 `payload_terminals_without_wires / 输入端子未被导线引用`,中文报告显示“未被 wires[] 引用的端子”,并在 `QetDiagnosticJson.unreferenced_payload_terminal_samples[]` 中保留设备标签、端子显示名、端子 UUID 和端子实例 ID。这个诊断用于暴露类似 `ID:27/as` 这类“现场预期有线,但当前 wires[] 没有任务”的疑点;它不是硬错误,因为端子也可能本来就是未接线端子。若现场确认该端子确实应该接线,应把样例发给 QET 侧核对导线导出逻辑或原理图连接数据。 + `RoutingPreflight` 还会附带 compact 路径网络诊断。若已标记 `CabinetInterior`,但主路径 carrier 或工程端子越出柜内边界,预检报告会直接追加 `route_carriers_outside_boundary` 或 `terminals_outside_boundary`,并在中文摘要中给出“越界路径”或“越界端子”样例。这样用户在生成导线前就能发现装配态问题。 预检的端点缺失示例会同时显示导线标签和端子对,例如 `导线 N4111,terminal-start -> terminal-missing`。这用于第一时间判断问题来自哪条 QET 导线任务、哪个端子 UUID 没有绑定到 FreeCAD 工程端子。 @@ -206,6 +210,8 @@ QetTemplateSlotName 4. 如果同一设备下 2D 端子数量和 3D 模板槽位数量一致,允许按顺序兜底匹配,但必须写诊断提示。 5. 仍无法匹配时,保留为 `local:*` 本地端子,不参与可靠自动布线。 +这里的“端子显示名/脚号”就是 QET 设备属性窗口和“编辑接线处”里看到的端子号,对应 `2d_to_3d.json` 中的 `terminal_display`,以及导线端点里的 `start_terminal_display / end_terminal_display`。例如 QET 接线处端子号为 `1`、`2`,导出后应分别作为对应端子的 `terminal_display`;FreeCAD 用它匹配 3D 模板中的 `QetTemplateSlotName=1/2`,也用它在 `terminal_uuid` 重复时辅助判断应该连接到哪一个 3D 端子。 + #### 1.5.4 QET 需要配合提供的数据 第一版数据库仍只使用 `project_2d3d_symbol_binding` 和 `project_2d3d_terminal_binding`。交换 JSON 中当前应优先使用已有的端子显示字段作为匹配提示: @@ -221,8 +227,12 @@ QetTemplateSlotName 这些字段只作为 FreeCAD 匹配模板槽位的提示,不写入第一版绑定表,也不能替代 `terminal_uuid`。 +其中 `terminal_display` 的来源就是 QET 端子号;它是人可读的端子/接线处编号,也是 3D 模板槽位匹配和重复端子消歧的重要提示字段。 + `slot_name_hint` 只是 FreeCAD 侧预留的可选扩展字段。当前 QET 如果没有该字段,不需要为了第一版专门增加;只要 `terminal_display` / `start_terminal_display` / `end_terminal_display` 能稳定表示设备脚号,就可以完成槽位匹配。 +当前测试工程的 v2 JSON 已经暴露出一个长期边界:`terminal_uuid` 和输入侧 `terminal_instance_id` 可能重复。FreeCAD 第一阶段会短期兜底,用 `device_instance_id / element_uuid / terminal_uuid / terminal_display` 识别具体 3D 端子,并在回写时生成不重复的 `terminal_instance_id`。但是如果 QET 侧仍按 `project_2d3d_terminal_binding(project_uuid, terminal_uuid)` 作为唯一键落库,就无法完整保存同一个 `terminal_uuid` 下的多个 3D 端子绑定。第一阶段 FreeCAD 不要求立即修改 QET 代码;后续 QET 若要完整消费 `3d_to_2d.json.terminals[]`,需要提供真正唯一的 2D 端子实例标识,或明确以 `terminal_instance_id` 作为 3D 端子绑定写回键。 + QET 侧还需要保证导线任务中继续提供: ```json @@ -332,7 +342,7 @@ FreeCAD 的 `3D 布线连接` 面板提供“主路径桥接容差 mm”数值 `TerminalAccess` 定位为端子局部接入线,只用于把端子出口引到柜内主路径附近。最终导线的主路径搜索不会把 `TerminalAccess` 当作公共 transit carrier,也不会用它桥接两段线槽或 `UserPath` 的缺口;入口候选排序也会优先选择线槽、`UserPath`、过线孔等真实主路径,避免导线贴到其它端子的局部接入线上起步。这类缺口应通过线槽、`UserPath`、过线孔或主路径自动桥接来解决。 -路径网络检查会诊断异常长的 `TerminalAccess`。当端子接入段明显过长时,报告会提示“端子接入过长”,建议补设备局部路径、移动设备,或补一段 `UserPath` / 线槽靠近端子。这类诊断用于避免设备未摆放好时生成看起来悬空或穿越设备区域的接入线。 +路径网络检查会诊断异常长的 `TerminalAccess`。当端子接入段明显过长时,报告会提示“端子接入过长”,并显示接入目标路径,例如远处线槽、`UserPath` 或 `RoutingRange` 布线面。`long_terminal_accesses[]` 会保留 `target_kind / target_name / target_label / target_rule / target_distance_mm`,用于判断问题是主路径入口离端子太远、局部出线路径缺失,还是当前只能退回布线面。处理建议是补设备局部路径、移动设备,或补一段 `UserPath` / 线槽靠近端子。这类诊断用于避免设备未摆放好时生成看起来悬空或穿越设备区域的接入线。 面板还提供“并行线间距 mm”、“并行线最大偏移 mm”和“并行线方向”,用于控制多根导线共用同一路径时的可视 lane 偏移。方向默认 `auto`,也可以手动指定 `x`、`y`、`z`。最大偏移用于限制密集共路时的显示错位范围,避免 lane 序号过大时把导线显示到线槽或柜体外。这些设置只影响 3D 显示上导线之间的错位方式,不代表真实线槽截面内的排布位置。 @@ -528,7 +538,9 @@ terminal_access_max_distance = 1000.0 `terminal_exit_length` 决定端子出线段长度;`terminal_access_max_distance` 决定端子出线点到最近路由网络的最大允许接入距离,并同时约束最终导线路由入口候选,避免用户调小端子接入距离后,最终求路仍跨很远接入孤立网络。两个参数都只保存在当前 FreeCAD 面板/调用选项中,不写数据库。 -网络检查发现端子未接入时,诊断 JSON 会记录该端子到最近路由网络的距离、当前端子接入最大距离和端子出线长度;面板报告会显示当前最大接入距离,便于判断是设备/线槽位置还没摆好,还是需要临时调大接入阈值。 +网络检查发现端子未接入时,诊断 JSON 会记录该端子到最近路由网络的距离、当前端子接入最大距离和端子出线长度;同时保留 `terminal_origin`、`terminal_exit_point`、`terminal_access_points`、`nearest_network_point`、`nearest_network_carrier_kind/name/label` 和接入折线的轴向长度。这样手动验收时可以直接判断问题来自端子 LCS/出线方向、设备摆放距离、主路径入口缺失,还是最近的线槽/UserPath/过线孔已经存在但没有接到端子局部路径。面板报告会显示当前最大接入距离,便于判断是设备/线槽位置还没摆好,还是需要临时调大接入阈值。 + +网络检查和批量布线摘要也会记录端子出线校正/截断样例。`corrected_terminal_exits[]` 与 `capped_terminal_exits[]` 会保留 `origin`、`exit_point`、`exit_direction`、`original_exit_direction`、`requested_exit_length_mm`、`actual_exit_length_mm`、`max_exit_length_mm`、`device_exit_required_length_mm`、`local_route_used` 和 `local_route_point_count`。这样可以直接判断问题是默认 LCS 朝向不合适、显式方向朝向设备内部、设备包围盒过大,还是已经使用局部出线路径但局部路径本身不合理。 ### 4.4 悬空线策略 @@ -639,11 +651,11 @@ QetWiringCutOutBridgeExtensionMm = 20.0 `route_samples[]` 不是简单截取前几条导线,而是优先保留带 `issue_codes` 的问题路线;问题数量相同或没有问题时,再按原生成顺序保留。这样当一次布线有很多正常线、少量异常线时,压缩诊断对象仍会优先给出异常样例,避免手动测试复制 JSON 后看不到真正需要处理的导线。 -一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中,并会按诊断建议先生成必要的 `UserPath` 桥接。脚本或调试场景直接调用 `route_eplan_connection_tasks()` 时,也会先执行同一类诊断桥接,保证任务入口和面板入口都优先尝试把孤立线槽接入端子主网络。直接从 QET payload 生成批量布线时,如果发现导线已经生成但没有使用线槽、`UserPath` 或过线孔主路径,也会自动补一次路径网络诊断,并把线槽未接入端子主网络、桥接建议等根因写回同一份批量报告。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长、端子越出柜内边界、路径越出柜内边界等问题带出来。如果路径源本身越出 `CabinetInterior`,批量报告会额外显示“越界路径:<路径标签> N 个越界点”,便于直接定位错误的线槽中心线或 `UserPath`。如果工程端子越出边界,批量报告会显示“越界端子:<端子对象/UUID> N 个越界点”,便于直接定位未装配到柜内的设备端子。 +一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中,并会按诊断建议先生成必要的 `UserPath` 桥接。脚本或调试场景直接调用 `route_eplan_connection_tasks()` 时,也会先执行同一类诊断桥接,保证任务入口和面板入口都优先尝试把孤立线槽接入端子主网络。直接从 QET payload 生成批量布线时,如果发现导线已经生成但没有使用线槽、`UserPath` 或过线孔主路径,也会自动补一次路径网络诊断,并把线槽未接入端子主网络、桥接建议等根因写回同一份批量报告。`auto_diagnostic_bridges` 摘要会保留未接入端子接入段的目标数、生成数、重复数和配对标签;中文报告会显示“未接入端子接入段 X 个,生成 Y 条”,便于判断自动补的是端子接入段到最近路径的短桥,而不是普通线槽孤岛桥接。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长、端子越出柜内边界、路径越出柜内边界等问题带出来。如果路径源本身越出 `CabinetInterior`,批量报告会额外显示“越界路径:<路径标签> N 个越界点”,便于直接定位错误的线槽中心线或 `UserPath`。如果工程端子越出边界,批量报告会显示“越界端子:<端子对象/UUID> N 个越界点”,便于直接定位未装配到柜内的设备端子。 真实工程中路径 carrier 数量可能达到数百个,入口候选组合会直接影响批量布线耗时。第一版保留单根布线的 `network_entry_candidate_limit`,同时在批量布线中增加 `batch_network_entry_candidate_limit`,默认按更保守的候选数求路,避免 `入口候选 x 出口候选 x 导线数量` 过度放大。批量入口候选还增加了总量保护 `batch_network_entry_total_candidate_limit`,当前默认值为 6;它会限制单根导线最终参与组合评分的入口/出口候选总量,避免“距离候选 + 柜内候选 + 避障候选”叠加后把一次布线放大成几十次 Dijkstra 求路。缺路径重试仍可以按 `missing_route_retry_candidate_limit` 临时放宽候选数量,但正常批量路径优先受总上限保护。批量布线还会复用本次已构建的基础路径图,避免每根导线重复构建同一套网络;碰撞障碍物也会先收集成候选缓存,再按每根导线的端点设备和端点附近规则过滤,避免重复扫描数千个模型对象。当前批量默认采用性能优先的 `batch_avoid_obstacles=false`:不额外构建障碍过滤图,但仍会在生成后做碰撞诊断并输出 `collision_warnings`;需要更激进避障时再开启批量障碍过滤。相关参数会写入 `RoutingConnectionBatch.QetDiagnosticJson.batch_network_entry_candidate_limit`、`batch_network_entry_total_candidate_limit`、`batch_avoid_obstacles` 和 `batch_obstacle_candidates`,便于手测时确认当前性能保护是否生效。 -线槽接入主网络采用保守桥接策略。当前 `adjoining_duct_tolerance` 默认只允许 5mm 内的相邻端点或端点到主路径中段投影自动桥接,不会为了让线槽被使用而把远距离线槽强行接到布线面或端子接入网络。这样可以避免误把柜内无关路径连成一个错误网络。若诊断出现 `wire_ducts_without_terminal_access / 线槽未接入端子主网络`,第一版推荐用户显式添加 UserPath、线槽开口或桥接路径;诊断会在 `bridge_suggestion` 中给出建议连接的两段 carrier、最近点和距离。面板已提供 `按诊断建议生成桥接`,用于先刷新诊断再按明确建议生成桥接;也提供 `选中两路径生成桥接`,用于在用户选中的两个路径 carrier 最近点之间生成一段 `UserPath`。这两个能力都属于半自动路径网络编辑,不会扫描全柜并自动连接所有远距离线槽。对于 UserPath 端点正好落在线槽中段的 0mm 接入,路径图会把被接入的线槽段在该点切开并并网,避免视觉上已经接触但路径组件仍被诊断为孤立。 +线槽接入主网络采用保守桥接策略。当前 `adjoining_duct_tolerance` 默认只允许 5mm 内的相邻端点或端点到主路径中段投影自动桥接,不会为了让线槽被使用而把远距离线槽强行接到布线面或端子接入网络。这样可以避免误把柜内无关路径连成一个错误网络。若诊断出现 `wire_ducts_without_terminal_access / 线槽未接入端子主网络`,第一版推荐用户显式添加 UserPath、线槽开口或桥接路径;诊断会在 `bridge_suggestion` 中给出建议连接的两段 carrier、最近点和距离。面板已提供 `按诊断建议生成桥接`,用于先刷新诊断再按明确建议生成桥接;也提供 `选中两路径生成桥接`,用于在用户选中的两个路径 carrier 最近点之间生成一段 `UserPath`。这两个能力都属于半自动路径网络编辑,不会扫描全柜并自动连接所有远距离线槽。对于 `unconnected_terminals[]` 中已经明确记录 `access_carrier` 和 `nearest_network_carrier_name/label` 的样例,诊断桥接会生成 `UnconnectedTerminalAccessBridge`,把该端子的 `TerminalAccess` 接入段补到最近路径;报告会单独输出 `unconnected_terminal_access_bridge_targets`、`unconnected_terminal_access_user_path_bridges`、`unconnected_terminal_access_bridge_duplicates` 和 `unconnected_terminal_access_bridge_pair_labels`,用于审计这类桥接和普通线槽桥接、端子退回补桥的区别。这个动作仍只依赖 FreeCAD 当前几何网络,不要求 QET 提供 3D 路径。对于 UserPath 端点正好落在线槽中段的 0mm 接入,路径图会把被接入的线槽段在该点切开并并网,避免视觉上已经接触但路径组件仍被诊断为孤立。 孤立路径网络诊断只针对可行动的路径组件。线槽、UserPath、过线孔、辅助路径和端子接入如果分成多个组件,会继续输出 `isolated_network_components`;但纯 `RoutingRange` 布线面孤岛只作为兜底网格保留在 `components` 明细中,不再单独触发“存在孤立路径网络”问题码。这样可以避免真实工程中安装板/布线面网格被误当作主路径断网问题,手测时优先处理线槽、用户路径和端子局部接入。 @@ -834,7 +846,11 @@ tests/python/freecad_exchange_auto_routing_test.py 29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻主路径端点桥接和端点到中段投影桥接容差,并在网络结果中记录桥接段数量。 30. `3D 布线连接` 面板提供“主路径桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 -32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 +32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离、端子出线长度、端子出口点、端子接入折线、最近网络点和最近网络对象 kind/name/label,并在中文报告里显示最大接入距离。 +32.1. 当 `TerminalAccess` 只能退回 `RoutingRange` 布线面时,路径网络诊断会记录退回目标、最近线槽/UserPath/过线孔主路径、最近主路径距离和当前最大接入距离;这些字段只来自 FreeCAD 当前几何网络,不要求 QET 提供。 +32.2. 当 `TerminalAccess` 为了避开端点设备包围盒而绕行时,路径网络诊断会记录 `endpoint_device_avoided`、`endpoint_device_bbox`、`access_points[]` 和 `access_length_mm`,用于判断接入段是否仍可能穿过设备或是否需要修正设备模板局部路径。 +32.3. `按诊断建议生成桥接` 处理 `terminal_access_fallback_targets[]` 时,会优先使用诊断样例中的 `nearest_main_path_name / nearest_main_path_label` 作为桥接目标;如果该对象在当前文档中找不到,才退回到最近主路径。这样能让“诊断看到的问题”和“自动补桥的对象”保持一致。 +32.4. `unconnected_terminals[]` 如果同时记录 `access_carrier` 和 `nearest_network_carrier_name/label`,推荐动作会提示点击 `按诊断建议生成桥接`,并生成 `UnconnectedTerminalAccessBridge` 把该端子的接入段补到最近路径;控制器报告会保留目标数、生成数、重复数和 `接入段 -> 最近路径` 配对标签,方便手动验收时确认补的是哪个端子的局部接入缺口。 33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 34. 批量布线报告会显示最大 lane 编号、lane 间距和最大偏移,方便确认多根线共路时是否发生了可视错位,以及偏移上限是否参与显示。 35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 @@ -881,7 +897,7 @@ tests/python/freecad_exchange_auto_routing_test.py 68. 面板提供“选择碰撞导线”,从最新批量诊断 `collision_samples[]` 和带 `collision_warnings` 的 `route_samples[]` 中反向选择 RoutedConnection 导线对象,便于和高发碰撞对象一起核对穿模位置。该功能只定位导线,不重新求路。 68. 面板提供“选择缺主路径导线”,从最新 `route_samples[]` 和导线对象自身的 `QetRouteIssueCodes` 中选择带 `main_path_detour_missing` 的 RoutedConnection 导线。该功能用于定位“选择性避障重算本可减少碰撞,但会退回到辅助路径/布线面兜底,因此被当前主路径优先策略拒绝”的导线;下一步应补 `UserPath`、桥接主路径、调整线槽入口或完善设备局部出线路径,不自动接受 fallback 结果。 68. 自动布线会对明确的 `main_path_detour_missing` 做一次收敛处理:当选择性避障已经得到碰撞更少的 fallback 折线,但该折线因包含 `RoutingRange` 被拒绝时,系统会把这条折线固化为 `MainPathDetourPath` 类型的 `UserPath`,再按 `兜底区域 -> 当前主路径` 生成 `MainPathDetourBridge`,随后只重试受影响导线。这样保持主路径优先,不直接接受宽泛布线面兜底,同时避免整批导线二次全量重跑。 -69. 面板提供“选择长接入端子”,从最新批量诊断 `routing_path_network_diagnostic.long_terminal_accesses[]` 中反向选择端子对象,便于检查端子高度、设备装配和局部出线路径。该功能只定位端子,不修改端子或路径数据。 +69. 面板提供“选择长接入端子”,从最新批量诊断 `routing_path_network_diagnostic.long_terminal_accesses[]` 中反向选择端子对象;当样例记录了 `access_carrier` 和 `target_name/target_label` 时,会同时选中端子的 `TerminalAccess` 接入段和目标路径,便于直接判断长接入是设备局部路径缺失、主路径入口过远,还是退回了布线面。该功能只定位对象,不修改端子或路径数据。 70. 面板提供“选择缺端子设备”,从最新批量诊断 `missing_endpoint_samples[]` 的缺失侧读取 `*_instance_id` / `*_element_uuid` 并反向选择 3D 设备,便于补工程端子或检查 2D/3D 绑定。若缺失设备不在当前场景中,控制器仍会返回 `missing_terminal_device_instance_ids[]`、`missing_terminal_device_element_uuids[]` 和可读标签,状态栏也会显示 instance_id,便于把缺设备清单交给装配/绑定流程。该功能只定位设备,不自动创建端子、不修改 QET 数据。 71. 面板提供“选择缺端子另一端”,从缺端子样例中选择已找到的另一端工程端子,便于确认失败导线本来要连接到哪里,再对照缺失侧设备和端子脚号。该功能只定位端子,不自动补端子、不写数据库。 72. 面板提供“选择缺端子候选端子”,从 `*_instance_terminal_samples` / `*_element_terminal_samples` 中反向选择同设备或同实例已有工程端子,便于排查 `terminal_uuid_not_in_element` 这类“同设备已有端子但 UUID 不匹配”的问题。该功能只定位候选端子,不自动改绑定、不写数据库。 @@ -1123,7 +1139,7 @@ QetTerminalLocalRoutePointsJson 导入/更新工程端子时,FreeCAD 会把 `local_route_points` 写入该端子的 `QetTerminalLocalRoutePointsJson`。后续自动生成 `TerminalAccess` 和最终导线几何时都会使用这段局部路径。 -路径网络检查会校验端子局部路径元数据。`QetTerminalLocalRoutePointsJson` / `QetLocalRoutePointsJson` 必须是 JSON 数组,或包含 `points` / `route_points` / `local_points` 数组的 JSON 对象,并且至少能解析出两个不同的有效点;如果 JSON 格式错误、没有可识别的点数组或有效点不足,诊断对象会记录 `invalid_terminal_local_routes`,中文报告会提示“端子局部路径无效”。这类问题不会让 FreeCAD 依赖 QET 提供 3D 路径,只是提示模板端子或工程端子的 3D 局部出线元数据需要修正。 +路径网络检查会校验端子局部路径元数据。`QetTerminalLocalRoutePointsJson` / `QetLocalRoutePointsJson` 必须是 JSON 数组,或包含 `points` / `route_points` / `local_points` 数组的 JSON 对象,并且至少能解析出两个不同的有效点;如果 JSON 格式错误、没有可识别的点数组或有效点不足,诊断对象会记录 `invalid_terminal_local_routes`,中文报告会提示“端子局部路径无效”。如果局部路径点数有效,但最后一个局部出口点仍落在端点所属设备包围盒内,诊断原因会记录为 `local_route_end_inside_device_bbox`,并保留 `local_route_end_point` 与 `endpoint_device_bbox`;实际生成 TerminalAccess 时会忽略这条无效局部路径,回退到设备感知默认出线,避免继续从设备内部接入主路径。这类问题不会让 FreeCAD 依赖 QET 提供 3D 路径,只是提示模板端子或工程端子的 3D 局部出线元数据需要修正。 如果直接在 FCStd 模板端子 LCS 上维护,也可以给模板端子写入同名属性 `QetTerminalLocalRoutePointsJson`。当前模板作者工具提供了内部函数: @@ -1300,6 +1316,14 @@ PE 线优先路径 13. 保存 FreeCAD 文档后,自动导线和路由网络仍保留。 14. 如果 `wires[].wire_style_id` 能在 `wire_properties` 中解析,生成导线会使用对应的显示颜色、线宽和线型;解析失败时诊断显示 `Missing`,但仍按默认蓝色样式生成导线。 15. “生成布线连接”后的 `RoutingConnectionBatch` 诊断对象保存最终 report,包括 `hidden_route_carriers`、`routing_path_network_updated`、路径网络检查结果和 `no_routed_connections` 等问题码。 +16. “生成布线连接”后会默认隐藏 `WireDuct` / `RoutingRange` / `TerminalAccess` 等 route carrier 辅助对象,同时强制显示 `QETWiring_04_Routed` 和 `RoutedConnection` 导线对象;批量诊断会写入 `routed_wire_visibility` 和 `route_carrier_visibility`。 +17. 如果导线带有 `wire_style_id` 或已解析的 `QetWireStyleJson`,但没有实际写入 `ViewObject` 显示样式,诊断会写入 `wire_style_application.missing_application` 并追加 `wire_styles_not_applied` 问题码。 +18. 黑色导线通过 `QetWireStyleApplied` 和 `QetAppliedWireLineColorRgb` 判断:`QetWireStyleApplied=true` 且 RGB 为 `0,0,0` 表示数据库样式本身是黑色;有样式 ID 但 `missing_application>0` 才表示样式未实际渲染。 +19. 对当前 v2 JSON 中 `terminal_uuid` 或 `terminal_instance_id` 重复的短期风险,FreeCAD 会在导入和布线预检时按 `device_instance_id / element_uuid / terminal_uuid / terminal_display / slot_name_hint` 消歧,并为重复或缺失的 `terminal_instance_id` 生成稳定 3D 端子实例 ID;该 ID 不依赖 QET 导出顺序,`3d_to_2d.json` 回写时禁止退化成设备 `device_instance_id`。 +20. 正式 QET 工程中的端子排、断路器批量装配优先走 `待装配设备 -> 批量插入同组到选中目标`,沿导轨按显示编号顺序插入真实 QET 设备并同步工程端子;`3D手动布线` 面板中的批量端子排/断路器按钮只作为旧流程或无 QET 数据时的演示兜底。 +21. 对已保存的旧工程或现场调试工程,`整理验收视图` 可以不重跑布线地隐藏 route carrier 辅助对象,并显示/重刷 `04_Routed` 导线样式;它用于 GUI 验收视图整理,不改变 QET 数据和布线路径结果。 +22. 端子导入和重导入时,FreeCAD 只把 `QETDevice_*` 设备组当作父设备;历史端子对象即使带有相同 `QetElementUuid`,也不能被误当成设备组。这样可以避免多 2D 元件共用同一 3D 设备实例时,把新端子挂到旧端子对象下面。 +23. 写回 `3d_to_2d.json` 前,如果能从传入 payload、`QET_2D_TO_3D_JSON` 或场景同目录读取当前 `2d_to_3d.json`,FreeCAD 会先按该快照同步工程端子,再收集 `instances[] / terminals[]`。保存 FCStd 时会在保存开始阶段先执行同一同步,使端子修复也进入 FreeCAD 工程文件。这样旧工程保存时也能回写完整端子绑定,例如当前测试工程应输出 `instances=86 / terminals=142`,且 `terminal_instance_id` 不重复。 ## 10. 开发验证命令 diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index 1d04455..e9b9f95 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -468,14 +468,16 @@ QET模板 -> 导入模板实例 1. 从 QET 点击 `3D视图` 打开 FreeCAD,确认树目录中已经有断路器设备。 2. 选中要安装断路器的导轨。 3. 切换到 `QET模板`。 -4. 打开 `3D手动布线`。 -5. 点击 `批量断路器`。 -6. 在 `QET断路器前缀` 中输入实际设备前缀,例如 `QF`。 -7. 输入断路器间距和起始偏移。 -8. 确认后,系统会把 QET 已导入的真实断路器沿导轨排布。 -9. 如果状态提示 `已排布 QET 断路器`,说明没有生成假设备,原有 QET 绑定仍保留。 +4. 打开 `待装配设备`。 +5. 在清单中选中同组断路器中的任意一个,例如 `QF1` 或 `QF:1`。 +6. 设置 `批量间距` 和 `起点偏移`。 +7. 点击 `批量插入同组到选中目标`。 +8. 系统会按显示编号顺序,把同一前缀的 QET 真实设备沿导轨排布,并在插入后同步工程端子。 +9. 如果只想插入单个设备,仍使用 `插入到选中目标`。 -只有当前工程没有 QET 断路器数据、只是做 3D 演示时,才使用兜底数量和兜底端子号生成本地演示对象。 +批量插入完成后,状态栏会显示本次匹配到的同组前缀、插入数量和前几个设备标签,例如 `已批量插入同组 QF 待装配设备 8 个(QF1、QF2...)`。如果只插入了 1 个,通常表示当前待装配清单中只有这个前缀的一个设备,或选中的设备标签没有和其它同组设备形成同一前缀,应先刷新清单并检查 QET 设备显示名。 + +`3D手动布线` 面板里的 `批量断路器` 属于旧流程/演示兜底:它会优先排布已有 QET 设备,但找不到匹配设备时会生成本地演示对象。正式 QET 工程优先使用 `待装配设备`,避免生成没有真实 2D 端子绑定的假设备。 常见间距: @@ -495,12 +497,14 @@ QET模板 -> 导入模板实例 2. 确认树目录中已经有 `UD`、`ID` 等端子排相关设备。 3. 选中要安装端子排的导轨。 4. 切换到 `QET模板`。 -5. 打开 `3D手动布线`。 -6. 点击 `批量端子排`。 -7. 在 `QET端子排名称/前缀` 中输入 `UD` 或 `ID`。 -8. 输入端子片间距,例如 `5.2 mm`,以及起始偏移。 -9. 确认后,系统会把匹配的 QET 真实端子片沿导轨按顺序排布。 -10. 如果状态提示 `已排布 QET 端子排`,说明工程端子和 `terminal_uuid` 没有被替换成本地端子。 +5. 打开 `待装配设备`。 +6. 在清单中选中同组端子排中的任意一个,例如 `UD:1`、`UD:8` 或 `ID:6`。 +7. 设置 `批量间距`,端子片常用 `5.2 mm`,再设置 `起点偏移`。 +8. 点击 `批量插入同组到选中目标`。 +9. 系统会按显示编号顺序,把同一前缀的 QET 真实端子片沿导轨排布,并在插入后同步工程端子。 +10. 如果只想插入单个端子片,仍使用 `插入到选中目标`。 + +状态栏会显示端子排前缀和前几个端子片标签,便于确认是否插入了正确的 `UD`、`ID` 等同组真实 QET 设备。这个批量入口不会生成本地假端子片;它只排布仍处于待装配状态的 QET 设备,并在插入后同步工程端子。 例如本仓库生成的端子片: @@ -522,6 +526,8 @@ data/examples/qet_terminal_block/qet_terminal_slice.FCStd 4. X 方向间距设为 `5.2 mm`。 5. 这种方式生成的端子通常是本地演示端子,不作为正式 QET 布线匹配主流程。 +`3D手动布线` 面板里的 `批量端子排` 仍保留,主要用于旧工程和无 QET 数据的演示兜底。第一阶段正式验收时,端子排批量装配应优先走 `待装配设备 -> 批量插入同组到选中目标`,这样 2D 端子、3D 设备和工程端子仍然来自同一份 QET v2 交换数据。 + ### 9.4 摆放电流互感器 电流互感器一般装在安装板或导轨附近。 @@ -747,7 +753,7 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 11. 选择线槽,点击 `标记为线槽`。 12. 用 `QET模板 -> 导入模板实例` 导入断路器、互感器、端子排等 FCStd 模板。 13. 用 `Assembly` 或 `Placement` 把设备摆到导轨上。 -14. 点击 `QET模板 -> 生成工程端子`。 +14. 正常 QET 流程下,FreeCAD 会在导入 `2d_to_3d.json` 时自动生成/更新工程端子;只有预检提示缺工程端子,或设备是手工导入模板时,才点击 `QET模板 -> 生成工程端子` 兜底。 15. 打开 `3D手动布线`。 16. 选择导线任务,或手动选起点端子。 17. 沿线槽添加折点。 @@ -763,7 +769,7 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 2. 导轨已经贴到安装板或背板上。 3. 线槽已经放到柜内,或已经用草图/Draft 线定义用户主路径。 4. QET 导入的真实设备实例已经摆到导轨或安装板上。 -5. 已点击 `生成工程端子`,工程端子能在 `QETTerminals_*` 分组中看到。 +5. 工程端子能在 `QETTerminals_*` 分组中看到;正常 QET 导入会自动生成,`生成工程端子` 只作为异常兜底按钮。 6. 如需限制导线不能跑出柜外,选择柜内空间、柜体或辅助包围盒,点击 `选中对象作为柜内边界`。 完成后按下面顺序检查: @@ -789,6 +795,12 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 每次点击 `检查布线准备度`,树目录 `QETWiring_05_Diagnostics` 下会刷新一个 `RoutingPreflight` 诊断对象。该对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示预检是否通过,`QetDiagnosticIssueCodes` 直接列出问题码,`QetDiagnosticIssueLabels` 直接列出中文问题标签,`QetDiagnosticMessage` 是中文摘要;展开属性中的 `QetDiagnosticJson`,可以查看缺失端点、路径源数量、柜内边界数量、路径网络诊断和导线样式库状态。这个对象只保存最新一次预检结果,避免多次测试后诊断对象堆积。 +如果报告出现 `duplicate_3d_terminal_uuids / 3D端子UUID重复`,说明当前 FreeCAD 文档里多个工程端子对象带有同一个 `QetTerminalUuid`。这不是立刻禁止布线的硬错误,但说明不能只靠端子 UUID 定位端子;FreeCAD 会继续使用导线端点的 `element_uuid`、3D 设备 `instance_id` 和端子显示名/脚号来消歧。如果后续同时出现缺端点、端子 UUID 不匹配或只接到错误设备,应优先检查 QET 导线端点是否带有 `element_uuid`,以及当前 3D 设备是否已经正确装配和绑定。 + +这里的端子显示名/脚号就是 QET 界面“编辑接线处”里的端子号,对应 `2d_to_3d.json` 的 `terminal_display`,以及 `wires[]` 端点中的 `start_terminal_display / end_terminal_display`。手测缺端子或接错端子时,可以直接用诊断里的 `*_terminal_display` 回到 QET 界面核对端子号。 + +如果报告出现 `payload_terminals_without_wires / 输入端子未被导线引用`,说明 `2d_to_3d.json` 的 `devices[].terminals[]` 中存在某个端子,但当前 `wires[]` 没有任何导线端点引用它。面板中文报告会显示“未被 wires[] 引用的端子”,诊断 JSON 的 `unreferenced_payload_terminal_samples[]` 会给出设备标签和端子显示名,例如 `ID:27/as`。这不一定是错误,因为端子可能本来未接线;如果现场确认该端子应该有线,应把这个样例交给 QET 侧检查原理图连接或 `wires[]` 导出。 + `检查布线准备度` 默认不再抽样求解导线可达性,避免真实机柜中大量设备、路径 carrier 和障碍对象导致预检长时间卡住。需要排查少量导线是否能连通时,再把面板里的 `可达性抽样` 数量从 `0` 调到 1、5 或更高;这个抽样只用于诊断,不影响正式点击 `生成布线连接` 时的全量布线。 预检阶段也会读取路径网络诊断摘要。如果已经标记 `CabinetInterior`,但工程端子或主路径 carrier 越出柜内边界,`检查布线准备度` 会显示“路径网络检查提示”,并带出“越界端子”或“越界路径”样例。这样可以在生成导线前发现装配位置、端子 LCS 或用户路径本身的问题。 @@ -805,7 +817,7 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 面板中的 `端子接入警告距离 mm` 用于判断“端子接入过长”。设为 `0` 时按默认规则自动计算;如果当前机柜尺度较大,且 600-700mm 的端子接入属于可接受的设备局部出线,可以把该值调到 700mm 左右再检查。这个参数只影响质量告警,不会放宽 `端子接入最大距离 mm`,也不会让超过最大距离的端子强行接入。 -如果路径网络诊断包含 `unconnected_terminals`,点击 `选择未接入端子`。系统会从最新 `RoutingPathNetwork` 诊断中选择未接入路由网络、或端子出口到最近网络距离超过 `端子接入最大距离 mm` 的端子及所属设备;状态栏会显示本次样例里的最大最近网络距离。选中后先确认设备是否已经装配到柜内正确位置,再看端子附近是否缺线槽入口、过线孔、黄色 `UserPath` 或设备局部出线路径;如果装配和路径都合理,但实际柜型允许更长的局部接入,再考虑调大 `端子接入最大距离 mm`。 +如果路径网络诊断包含 `unconnected_terminals`,点击 `选择未接入端子`。系统会从最新 `RoutingPathNetwork` 诊断中选择未接入路由网络、或端子出口到最近网络距离超过 `端子接入最大距离 mm` 的端子及所属设备;如果诊断样例里有 `access_carrier`,会同时选中该端子的 `TerminalAccess` 接入段;如果诊断样例里有 `nearest_network_carrier_name/label`,也会同时选中最近的线槽、过线孔、黄色 `UserPath` 或其它路径 carrier。状态栏会显示本次样例里的最大最近网络距离、接入段数量和最近路径数量。`QetDiagnosticJson.unconnected_terminals[]` 会保存 `terminal_origin`、`terminal_exit_point`、`terminal_access_points`、`nearest_network_point`、`nearest_network_distance_mm`、`nearest_network_carrier_kind/name/label` 和接入折线的主轴长度,便于判断是端子 LCS/出线方向问题、设备摆放距离过远,还是最近线槽、过线孔、黄色 `UserPath` 或设备局部出线路径没有接上。选中后先确认设备是否已经装配到柜内正确位置,再看端子附近是否缺主路径入口;如果装配和路径都合理,但实际柜型允许更长的局部接入,再考虑调大 `端子接入最大距离 mm`。 如果有线槽但导线仍大量走布线面,优先看 `RoutingPathNetwork.QetDiagnosticIssueCodes` 是否包含 `wire_ducts_without_terminal_access / 线槽未接入端子主网络`。这个问题表示线槽已经识别成路径 carrier,但它所在的路径组件没有任何 `TerminalAccess`,导线很难自然进入线槽。中文报告会尽量显示“建议桥接到哪个主网络”和最近距离;`QetDiagnosticJson.wire_ducts_without_terminal_access[].bridge_suggestion` 会保存建议连接的两段 carrier、两个最近点和距离。处理方式是在 FreeCAD 中用 UserPath、线槽开口或桥接路径,把线槽组件接到端子接入所在的主网络,再重新生成布线路径网络和导线。 @@ -913,10 +925,12 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 选中生成的导线对象后,可以在 `QetRouteDiagnosticsJson.endpoint_access.start_diagnostics / end_diagnostics` 中查看每侧端子的 `exit_rule`、`exit_direction_source`、`exit_direction`、`requested_exit_length_mm`、`actual_exit_length_mm`、`device_exit_required_length_mm` 和 `exit_length_capped`。如果 `exit_rule=local_route`,说明该端子正在使用 `QetTerminalLocalRoutePointsJson` 局部出线路径;如果 `exit_length_capped=true`,说明这侧端子按当前显式方向无法在合理长度内离开设备包围盒,后续容易出现端子附近悬空过长或穿模,应优先修正端子方向或给该端子设置局部出线路径。 -点击 `检查布线路径网络` 时,也会提前汇总端子出线问题。`corrected_terminal_exits[]` 表示默认 LCS 出线方向被系统自动改到最近侧向出口,通常说明设备模板端子方向还需要复查;`capped_terminal_exits[]` 表示端子按当前显式方向或默认方向无法在最大出线长度内离开设备包围盒,系统已经截断出线段。两个数组都会保留端子名、端子 UUID、父设备、原始方向、实际方向、请求长度、实际长度和上限,便于手动验收时先定位设备端子,再决定是修模板 CPoint、设置工程端子局部出线,还是补主路径入口。 +点击 `检查布线路径网络` 时,也会提前汇总端子出线问题。`corrected_terminal_exits[]` 表示默认 LCS 出线方向被系统自动改到最近侧向出口,通常说明设备模板端子方向还需要复查;`capped_terminal_exits[]` 表示端子按当前显式方向或默认方向无法在最大出线长度内离开设备包围盒,系统已经截断出线段。两个数组都会保留端子名、端子 UUID、父设备、原始方向、实际方向、端子原点 `origin`、实际出线点 `exit_point`、请求长度、实际长度、上限、是否使用局部路径 `local_route_used` 和局部路径点数,批量布线摘要也会保留这些字段。手动验收时可以先定位设备端子,再决定是修模板 CPoint、设置工程端子局部出线,还是补主路径入口。 如果 `QetTerminalExitDirectionJson` 格式错误、方向向量无法解析或方向长度为 0,路径网络诊断会额外输出 `invalid_terminal_exit_directions[]`。这种情况不会让 FreeCAD 依赖 QET 计算 3D 路径,而是明确提示当前 FreeCAD 文档或设备模板中的 CPoint 方向元数据需要修正;可以用 `选中端子设置出线方向` 重写当前工程端子的显式方向,或回到设备模板中修正后重新导入。 +如果 `QetTerminalLocalRoutePointsJson` 能解析出点,但局部路径终点仍在端点所属设备包围盒内,路径网络诊断会把该端子记入 `invalid_terminal_local_routes[]`,原因是 `local_route_end_inside_device_bbox`,并保留 `local_route_end_point` 和 `endpoint_device_bbox`。实际生成 `TerminalAccess` 时会忽略这条无效局部路径,回退到设备感知默认出线,避免继续从设备内部拉线。手动验收时应重新画一条真正离开设备外轮廓的局部路径,或修正设备模板端子 CPoint/RPoint。 + 如果要直接定位这些端子,点击 `选择出线问题端子`。系统会从最新 `RoutingPathNetwork` 诊断中合并选择 `corrected_terminal_exits[]`、`capped_terminal_exits[]`、`invalid_terminal_exit_directions[]` 和 `invalid_terminal_local_routes[]` 对应的端子及父设备;这个操作只负责定位,不会自动改端子方向或重新布线。选中后先看端子 LCS 朝向、显式 `QetTerminalExitDirectionJson`、局部路径 `QetTerminalLocalRoutePointsJson`、设备包围盒是否过大,再决定是否设置显式出线方向、设置局部出线路径或回到设备模板修正 CPoint。 每个自动生成的 `TerminalAccess` carrier 会记录接入目标:`QetTerminalAccessTargetKind / Name / Label / DistanceMm` 表示端子局部出口接到哪条线槽、`UserPath`、过线孔或面板路径;`QetTerminalAccessTargetRule` 表示选择规则,`main_path_nearest` 是直接接入最近主路径,`main_path_preferred_over_fallback` 是附近虽有 `RoutingRange` 等兜底路径但系统仍优先接入主路径,`fallback_only` 表示当前找不到线槽/UserPath/过线孔等主路径,只能退回面板路径或辅助路径。`QetTerminalAccessFallbackTarget=1` 时,应优先补线槽入口、黄色草图 `UserPath`、过线孔或设备局部路径,再重新生成布线路径网络。 @@ -925,15 +939,15 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 点击 `检查布线路径网络` 时,诊断 JSON 也会汇总 `terminal_access_fallback_targets[]` 和 `terminal_access_endpoint_device_avoidance[]`。前者表示某些端子接入只能退回 `RoutingRange` 等兜底路径,通常需要补线槽入口、`UserPath` 或过线孔;后者表示某些端子接入段已经为了避开端点设备做了绕行,后续我进行手动验收时会优先检查这些端子附近是否缺设备局部出线路径或主路径入口。这两个数组都包含端子名、端子 UUID、父设备、`TerminalAccess` 接入段对象名、目标路径类型、目标路径对象名、`access_points[]` 和 `access_length_mm`,便于自动定位对象并判断接入段是否过长、是否绕回设备附近。 -如果要定位端子接入退回到布线面的对象,点击 `选择端子退回位置`。该按钮既能读取独立 `RoutingPathNetwork.terminal_access_fallback_targets[]`,也能读取批量布线诊断里的端子退回样例;只执行 `检查布线路径网络`、还没有生成导线时,也可以先选中端子、父设备、`TerminalAccess` 接入段和退回目标,判断应该补线槽入口、黄色 `UserPath`、过线孔还是设备局部出线路径。 +如果要定位端子接入退回到布线面的对象,点击 `选择端子退回位置`。该按钮既能读取独立 `RoutingPathNetwork.terminal_access_fallback_targets[]`,也能读取批量布线诊断里的端子退回样例;只执行 `检查布线路径网络`、还没有生成导线时,也可以先选中端子、父设备、`TerminalAccess` 接入段和退回目标,判断应该补线槽入口、黄色 `UserPath`、过线孔还是设备局部出线路径。退回样例会记录 `target_kind / target_label / target_distance_mm`,并在存在主路径时记录 `nearest_main_path_kind / nearest_main_path_label / nearest_main_path_distance_mm / terminal_access_max_distance_mm`;如果最近主路径距离大于最大接入距离,通常说明需要在端子和线槽/UserPath 之间补桥接路径,或在确认工程允许时调大 `端子接入最大距离 mm`。 -如果这些退回目标只是缺一小段到主路径的入口,可以直接点击 `按诊断建议生成桥接`。该按钮现在既能读取批量布线诊断里的 `terminal_access_fallback_target_samples[]`,也能读取刚执行 `检查布线路径网络` 后生成的 `RoutingPathNetwork.terminal_access_fallback_targets[]`,自动在退回布线面和最近线槽、`UserPath`、过线孔等主路径之间生成 `TerminalAccessFallbackBridge`。生成后重新执行 `生成布线路径网络` 或 `生成布线连接`,端子接入会优先走补出的桥接路径;如果仍然退回布线面,说明需要补更明确的主路径入口或设备局部出线路径。 +如果这些退回目标只是缺一小段到主路径的入口,可以直接点击 `按诊断建议生成桥接`。该按钮现在既能读取批量布线诊断里的 `terminal_access_fallback_target_samples[]`,也能读取刚执行 `检查布线路径网络` 后生成的 `RoutingPathNetwork.terminal_access_fallback_targets[]`,自动在退回布线面和最近线槽、`UserPath`、过线孔等主路径之间生成 `TerminalAccessFallbackBridge`。如果诊断样例里已经记录了 `nearest_main_path_name / nearest_main_path_label`,桥接会优先连接这条诊断推荐主路径;找不到推荐对象时才退回到当前文档里最近的主路径。对于 `unconnected_terminals[]` 中同时存在 `access_carrier` 和 `nearest_network_carrier_name/label` 的样例,该按钮也会在端子 `TerminalAccess` 接入段和最近路径之间生成 `UnconnectedTerminalAccessBridge`,用于补齐端子接入段到线槽/UserPath 的短缺口;这类样例存在时,汇总诊断的推荐动作会明确提示点击 `按诊断建议生成桥接`。按钮报告会单独统计 `unconnected_terminal_access_bridge_targets`、`unconnected_terminal_access_user_path_bridges`、`unconnected_terminal_access_bridge_duplicates` 和 `unconnected_terminal_access_bridge_pair_labels`,状态栏会显示“未接入端子接入段 X 个,生成 Y 条”,便于区分这是未接端子补桥,不是线槽孤岛补桥或端子退回布线面补桥。生成后重新执行 `生成布线路径网络` 或 `生成布线连接`,端子接入会优先走补出的桥接路径;如果仍然退回布线面或未接入,说明需要补更明确的主路径入口或设备局部出线路径。 -如果要直接定位端点设备避让问题,点击 `选择端点避让接入`。系统会读取最新 `RoutingPathNetwork` 诊断中的 `terminal_access_endpoint_device_avoidance[]`,选中对应端子、父设备、目标主路径和 `TerminalAccess` 接入段;这个按钮主要服务手动验收和开发侧复查,只定位对象,不重新布线、不写 QET 数据库。 +如果要直接定位端点设备避让问题,点击 `选择端点避让接入`。系统会读取最新 `RoutingPathNetwork` 诊断中的 `terminal_access_endpoint_device_avoidance[]`,选中对应端子、父设备、目标主路径和 `TerminalAccess` 接入段;这个按钮主要服务手动验收和开发侧复查,只定位对象,不重新布线、不写 QET 数据库。诊断样例会记录 `endpoint_device_avoided`、`endpoint_device_bbox`、`access_points[]` 和 `access_length_mm`;如果后续仍看到导线穿设备,可以对照这些点和包围盒判断是端子局部路径方向不合理、父设备包围盒过大,还是主路径入口仍在设备背后。 -`检查布线路径网络` 和批量布线的 `routing_path_network_diagnostic.long_terminal_accesses[]` 会保留长接入样例。样例里包含 `parent_device_label / parent_device_name`、`terminal_origin`、`terminal_access_points`、`terminal_access_dominant_axis` 和 `terminal_access_axis_lengths_mm`。如果 `terminal_access_dominant_axis` 是 `z`,且 `z` 方向长度占大头,通常表示端子点和柜内主路径平面高度差过大;优先检查该设备装配高度、端子 LCS 方向,或为该设备补局部出线路径。 +`检查布线路径网络` 和批量布线的 `routing_path_network_diagnostic.long_terminal_accesses[]` 会保留长接入样例。样例里包含 `parent_device_label / parent_device_name`、`terminal_origin`、`terminal_access_points`、`terminal_access_dominant_axis`、`terminal_access_axis_lengths_mm`,以及接入目标 `target_kind / target_label / target_distance_mm`。如果目标是远处线槽或远处 `UserPath`,优先补设备局部出线路径或把主路径入口靠近端子;如果目标是 `RoutingRange` 布线面,优先补线槽入口、过线孔或黄色 `UserPath`。如果 `terminal_access_dominant_axis` 是 `z`,且 `z` 方向长度占大头,通常表示端子点和柜内主路径平面高度差过大;优先检查该设备装配高度、端子 LCS 方向,或为该设备补局部出线路径。 -如果要快速定位这些端子,点击 `选择长接入端子`。系统会从最新批量布线诊断中的 `routing_path_network_diagnostic.long_terminal_accesses[]` 查找端子对象并选中。真实工程中类似 PEN 325-328 这类端子被选中后,可以直接检查它们是否位于异常高度、是否缺设备局部出线路径,或附近是否缺主路径入口。 +如果要快速定位这些端子,点击 `选择长接入端子`。系统会从最新批量布线诊断中的 `routing_path_network_diagnostic.long_terminal_accesses[]` 查找端子对象并选中;如果样例里记录了 `access_carrier` 和 `target_name/target_label`,会同时选中该端子的 `TerminalAccess` 接入段和目标线槽/`UserPath`/布线面。真实工程中类似 PEN 325-328 这类端子被选中后,可以直接检查它们是否位于异常高度、是否缺设备局部出线路径,或附近是否缺主路径入口。 如果要从设备角度排查,点击 `选择长接入设备`。系统会读取长接入样例里的 `parent_device_name / parent_device_label` 并选中对应设备。通常先用 `选择长接入端子` 看具体端子点,再用 `选择长接入设备` 检查该设备整体是否装配到正确高度、端子 LCS 是否随设备移动,以及设备附近是否需要补局部出线路径。 @@ -1097,7 +1111,7 @@ FreeCAD 版本/运行目录: 3. 校验端子。 4. 保存为 FCStd。 5. 回到工程场景重新导入或更新设备。 -6. 点击 `生成工程端子`。 +6. 如果设备是通过 `待装配设备` 面板插入,当前版本会在插入后自动同步工程端子;如果设备是旧流程手工导入、模板刚修改过,或诊断提示缺工程端子,再点击 `生成工程端子` 做一次补生成。 ### 15.2 为什么不能布线? @@ -1160,7 +1174,29 @@ scene.FCStd QET 侧只依赖最小绑定字段找到对应设备和端子。 -### 15.5 当前截图里的 `Gears` 应该怎么处理? +### 15.5 为什么黑色线和辅助网格要看诊断字段? + +生成布线连接后,系统会把导线显示状态和辅助路径显示状态写入 `RoutingConnectionBatch` 诊断: + +- `routed_wire_visibility`:统计 `QETWiring_04_Routed` 下导线是否可见。GUI 中如果 `hidden>0`,说明生成后仍有导线不可见。 +- `route_carrier_visibility`:统计 `WireDuct`、`RoutingRange`、`TerminalAccess` 等辅助 carrier 是否仍可见。默认应隐藏,避免手动验收时把网格误认为导线。 +- `wire_style_application`:统计有样式数据的导线是否已经实际应用到 `ViewObject`。 +- `available_terminals` / `available_terminal_objects`:统计当前 FreeCAD 工程里的实际 3D 工程端子对象数量。`unique_terminal_uuids` 只表示去重后的 `terminal_uuid` 数量;当前 QET v2 数据里同一个 `terminal_uuid` 可能对应多个设备/脚号,所以手测时应优先看 `available_terminals` 是否接近 `2d_to_3d.json` 中的端子总数。 +- `duplicate_payload_terminal_instance_id_count`:统计 `2d_to_3d.json` 输入里重复的 `terminal_instance_id` 组数。第一阶段 FreeCAD 会按设备实例、端子 UUID、端子显示名生成稳定兜底 ID,保证 `3d_to_2d.json` 不把端子 ID 退化成设备 ID;但这仍表示 QET 输入数据有重复风险,后续建议 QET 侧提供真正唯一的端子实例 ID。 + +保存或手动执行写回时,如果 FreeCAD 能找到当前 `2d_to_3d.json`,会先按 JSON 快照同步工程端子,再生成 `3d_to_2d.json`。保存流程会在写入 FCStd 前执行这一步,因此保存下来的 FreeCAD 工程也会带上端子修复结果,而不是只修正回写 JSON。因此正常 QET 流程下不需要为了回写再单独点一次 `生成工程端子`;只有旧流程手工导入模型、模板刚改过、或诊断明确提示缺工程端子时,才把 `生成工程端子` 当作兜底修复按钮。 + +端子对象本身也会带 `QetElementUuid`,但它不能作为设备父级。当前版本查找父设备时只接受 `QETDevice_*` 设备组,避免旧工程里同 UUID 端子对象被误当作设备,导致新端子挂到旧端子下面。手测时如果发现 `QETTerminal_*` 下面又出现 `QETTerminals_*` 分组,说明工程可能来自旧版本,需要重新导入或保存触发端子同步后再检查。 + +黑色导线不一定是错误。若导线对象上 `QetWireStyleApplied=true`,并且 `QetAppliedWireLineColorRgb=0,0,0`,表示 `wire_properties` 中的线色本来就是黑色;若 `wire_style_application.missing_application>0`,才表示样式没有真正渲染。 + +批量布线中文报告也会显示 `黑色导线:N 条来自 wire_properties 样式`。看到这条提示时,表示这些黑线已经成功从导线样式库解析并应用,不是默认未渲染;只有同时出现 `导线样式实际应用异常` 或 `wire_styles_not_applied` 时,才按样式渲染失败处理。 + +在 FreeCADCmd 这类无 GUI 验证环境中,可见性可能显示为 `unknown_visibility`,这是因为 headless 模式读不到 `ViewObject.Visibility`,不代表 GUI 里一定隐藏。 + +如果打开旧工程后仍能看到很多布线路径网格、`RoutingRange` 面网格或 `TerminalAccess` 辅助线,但导线已经存在于 `04_Routed` 下,可以在 `3D布线连接` 面板点击 `整理验收视图`。该按钮不会重新布线、不会删除对象、不会写数据库;它只隐藏 route carrier 辅助对象,并显示/重刷 `04_Routed` 导线和导线样式,适合手动验收前整理最终视图。 + +### 15.6 当前截图里的 `Gears` 应该怎么处理? 这是 Assembly 的齿轮约束任务,不适合机柜装配。 @@ -1181,7 +1217,7 @@ QET 侧只依赖最小绑定字段找到对应设备和端子。 4. 导轨、线槽、机柜可作为纯几何资产。 5. 正式 QET 工程中,端子排和断路器优先排布 QET 已导入的真实实例;Draft 阵列只作为无 QET 数据时的手工演示方式。 6. 每完成一段装配就保存一次 `scene.FCStd`。 -7. 布线前先生成工程端子。 +7. 布线前确认工程端子已经生成:通过 `待装配设备` 插入的设备会自动同步工程端子;旧流程手工导入或诊断提示缺端子时,再点击 `生成工程端子`。 8. 生成布线连接前先建立布线路径网络。 9. 不要手动改工程绑定 UUID。 10. 不要依赖旧 3D 场景表保存位姿。 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 8026b8f..70502df 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -80,7 +80,8 @@ DEFAULT_OPTIONS = { "UserPath": 1.0, "AuxiliaryPath": 2.0, "TerminalAccess": 2.0, - "RoutingRange": 40.0, + # RoutingRange 是安装板/柜内面域兜底,不应因为线槽复用惩罚而抢走主线槽。 + "RoutingRange": 1000.0, }, # 主干必须走 carrier/贴面网络;没有布线路径网络时直接失败。 # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 @@ -112,6 +113,9 @@ DEFAULT_OPTIONS = { # 第一次布线若发现端子接入退回布线面/辅助路径, # 自动补一段到最近主路径的 UserPath 桥并重跑一次。 "auto_create_terminal_access_fallback_bridges": True, + # 第一次布线若发现两端已经接到线槽/UserPath 等主路径,但中段仍退回布线面, + # 自动补一段主路径目标之间的 UserPath 桥并重跑一次。 + "auto_create_main_path_target_bridges": True, } @@ -126,6 +130,31 @@ def _merged_options(options): return merged +def _route_network_cache(opts): + cache = opts.get("__route_network_cache") if isinstance(opts, dict) else None + return cache if isinstance(cache, dict) else None + + +def _invalidate_route_network_cache(opts): + cache = _route_network_cache(opts) + if cache is not None: + cache.pop("route_network", None) + + +def _cached_base_route_network(doc, opts): + cache = _route_network_cache(opts) + cached = cache.get("route_network") if cache is not None else None + if isinstance(cached, dict) and int(cached.get("segment_count", 0) or 0) > 0: + return cached, True + network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float((opts or {}).get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + if cache is not None and int(network.get("segment_count", 0) or 0) > 0: + cache["route_network"] = network + return network, False + + def _has_route_constraints(options): opts = options or {} for key in ( @@ -977,6 +1006,181 @@ def _terminal_uuid_duplicate_summary(terminal_candidates, limit=8): } +def _payload_terminal_instance_duplicate_summary(payload, limit=8): + counts = {} + if isinstance(payload, dict): + for device in list(payload.get("devices", []) or []): + if not isinstance(device, dict): + continue + for terminal in list(device.get("terminals", []) or []): + if not isinstance(terminal, dict): + continue + terminal_instance_id = str(terminal.get("terminal_instance_id", "") or "").strip() + if not terminal_instance_id: + continue + counts[terminal_instance_id] = counts.get(terminal_instance_id, 0) + 1 + samples = [] + for terminal_instance_id, count in sorted(counts.items()): + if count <= 1: + continue + if len(samples) < limit: + samples.append( + { + "terminal_instance_id": terminal_instance_id, + "count": count, + } + ) + return { + "duplicate_payload_terminal_instance_id_count": sum( + 1 for count in counts.values() if count > 1 + ), + "duplicate_payload_terminal_instance_id_samples": samples, + } + + +def _payload_wire_endpoint_refs(payload): + refs = [] + if not isinstance(payload, dict): + return refs + for wire in list(payload.get("wires", []) or []): + if not isinstance(wire, dict): + continue + for side in ("start", "end"): + terminal_uuid = str(wire.get("{0}_terminal_uuid".format(side), "") or "").strip() + if not terminal_uuid: + continue + refs.append( + { + "side": side, + "wire_uuid": str( + wire.get("wire_id", "") + or wire.get("wire_uuid", "") + or wire.get("id", "") + or "" + ).strip(), + "wire_label": str(wire.get("wire_label", "") or wire.get("wire_mark", "") or "").strip(), + "terminal_uuid": terminal_uuid, + "element_uuid": str(wire.get("{0}_element_uuid".format(side), "") or "").strip(), + "device_instance_id": str( + wire.get("{0}_instance_id".format(side), "") + or wire.get("{0}_device_instance_id".format(side), "") + or "" + ).strip(), + "terminal_display": str( + wire.get("{0}_terminal_display".format(side), "") + or wire.get("{0}_terminal_label".format(side), "") + or "" + ).strip(), + } + ) + return refs + + +def _payload_terminal_display(terminal): + if not isinstance(terminal, dict): + return "" + return str( + terminal.get("terminal_display", "") + or terminal.get("terminal_label", "") + or terminal.get("label", "") + or terminal.get("name", "") + or "" + ).strip() + + +def _payload_endpoint_matches_terminal(endpoint, device, terminal, duplicate_terminal_uuids): + terminal_uuid = str(terminal.get("terminal_uuid", "") or "").strip() + if not terminal_uuid or endpoint.get("terminal_uuid") != terminal_uuid: + return False + endpoint_element = str(endpoint.get("element_uuid", "") or "").strip() + terminal_element = str(terminal.get("element_uuid", "") or "").strip() + if endpoint_element and terminal_element and endpoint_element != terminal_element: + return False + endpoint_instance = str(endpoint.get("device_instance_id", "") or "").strip() + device_instance = str( + device.get("device_instance_id", "") + or device.get("instance_id", "") + or "" + ).strip() + if endpoint_instance and device_instance and endpoint_instance != device_instance: + return False + endpoint_display = _normalized_match_token(endpoint.get("terminal_display", "")) + terminal_display = _normalized_match_token(_payload_terminal_display(terminal)) + if endpoint_display and terminal_display and endpoint_display != terminal_display: + return False + if terminal_uuid in duplicate_terminal_uuids and not (endpoint_element or endpoint_instance or endpoint_display): + return False + return True + + +def _payload_unreferenced_terminal_summary(payload, limit=8): + if not isinstance(payload, dict): + return { + "unreferenced_payload_terminal_count": 0, + "unreferenced_payload_terminal_samples": [], + } + terminal_uuid_counts = {} + for device in list(payload.get("devices", []) or []): + if not isinstance(device, dict): + continue + for terminal in list(device.get("terminals", []) or []): + if not isinstance(terminal, dict): + continue + terminal_uuid = str(terminal.get("terminal_uuid", "") or "").strip() + if terminal_uuid: + terminal_uuid_counts[terminal_uuid] = terminal_uuid_counts.get(terminal_uuid, 0) + 1 + duplicate_terminal_uuids = { + terminal_uuid + for terminal_uuid, count in terminal_uuid_counts.items() + if count > 1 + } + endpoints = _payload_wire_endpoint_refs(payload) + samples = [] + count = 0 + for device in list(payload.get("devices", []) or []): + if not isinstance(device, dict): + continue + device_instance_id = str( + device.get("device_instance_id", "") + or device.get("instance_id", "") + or "" + ).strip() + device_label = str( + device.get("display_tag", "") + or device.get("label", "") + or device.get("name", "") + or device_instance_id + or "" + ).strip() + for terminal in list(device.get("terminals", []) or []): + if not isinstance(terminal, dict): + continue + terminal_uuid = str(terminal.get("terminal_uuid", "") or "").strip() + if not terminal_uuid: + continue + if any( + _payload_endpoint_matches_terminal(endpoint, device, terminal, duplicate_terminal_uuids) + for endpoint in endpoints + ): + continue + count += 1 + if len(samples) < int(limit or 0): + samples.append( + { + "device_label": device_label, + "device_instance_id": device_instance_id, + "element_uuid": str(terminal.get("element_uuid", "") or "").strip(), + "terminal_uuid": terminal_uuid, + "terminal_instance_id": str(terminal.get("terminal_instance_id", "") or "").strip(), + "terminal_display": _payload_terminal_display(terminal), + } + ) + return { + "unreferenced_payload_terminal_count": count, + "unreferenced_payload_terminal_samples": samples, + } + + def _terminal_endpoint_match(terminal_candidates, item, side, allow_single_fallback=True): terminal_uuid = _wire_item_value(item, "{0}_terminal_uuid".format(side)) result = { @@ -1035,6 +1239,22 @@ def _terminal_endpoint_match(terminal_candidates, item, side, allow_single_fallb best_score = matched[0][0] best_matches = [terminal for score, terminal in matched if score == best_score] if len(best_matches) > 1: + display_token = _normalized_match_token( + _wire_item_value( + item, + "{0}_terminal_display".format(side), + "{0}_terminal_label".format(side), + ) + ) + if display_token: + display_matches = [ + terminal + for terminal in best_matches + if display_token in _terminal_match_tokens(terminal) + ] + if len(display_matches) == 1: + result["terminal"] = display_matches[0] + return result result["ambiguous"] = True result["reason_code"] = "ambiguous_terminal_uuid_context" return result @@ -1287,6 +1507,116 @@ def _wire_endpoint_entries(payload): return entries +def _payload_device_terminal_groups(payload): + groups = {} + if not isinstance(payload, dict): + return groups + for device in list(payload.get("devices", []) or []): + if not isinstance(device, dict): + continue + instance_id = ( + str(device.get("device_instance_id", "") or "").strip() + or str(device.get("instance_id", "") or "").strip() + ) + if not instance_id: + continue + for terminal in list(device.get("terminals", []) or []): + if not isinstance(terminal, dict): + continue + terminal_uuid = str(terminal.get("terminal_uuid", "") or "").strip() + if not terminal_uuid: + continue + entry = { + "instance_id": instance_id, + "terminal_uuid": terminal_uuid, + "element_uuid": str(terminal.get("element_uuid", "") or "").strip(), + "terminal_instance_id": str(terminal.get("terminal_instance_id", "") or "").strip(), + "terminal_display": str( + terminal.get("terminal_display", "") + or terminal.get("terminal_label", "") + or "" + ).strip(), + } + groups.setdefault((instance_id, terminal_uuid), []).append(entry) + return groups + + +def _pair_payload_terminal_entries_with_objects(entries, terminal_objects): + remaining_entries = list(entries or []) + remaining_objects = list(terminal_objects or []) + pairs = [] + for entry in list(remaining_entries): + display_token = _normalized_match_token(entry.get("terminal_display", "")) + if not display_token: + continue + matches = [ + terminal + for terminal in remaining_objects + if display_token in _terminal_match_tokens(terminal) + ] + if len(matches) != 1: + continue + terminal = matches[0] + pairs.append((entry, terminal)) + remaining_entries.remove(entry) + remaining_objects.remove(terminal) + pairs.extend(zip(remaining_entries, remaining_objects)) + return pairs + + +def _repair_duplicate_terminal_metadata_from_payload(doc, payload): + """Use v2 device terminal rows to repair duplicated terminal_uuid metadata. + + QET v2 snapshots may describe several physical 3D terminals with the same + terminal_uuid. When an older/imported FreeCAD scene copied the first + terminal metadata across those objects, routing cannot safely choose an + endpoint. 这里仅在“同实例、同 terminal_uuid、数量完全一致”时按显示名/顺序修复, + 不额外创建端子,避免把未知设备猜错。 + """ + if doc is None: + return {"repaired": 0, "groups": 0} + groups = _payload_device_terminal_groups(payload) + if not groups: + return {"repaired": 0, "groups": 0} + terminals = _collect_routable_terminals(doc) + order = {id(terminal): index for index, terminal in enumerate(terminals)} + repaired = 0 + repaired_groups = 0 + project_uuid = str(payload.get("project_uuid", "") or _project_uuid(doc)).strip() if isinstance(payload, dict) else _project_uuid(doc) + for (instance_id, terminal_uuid), entries in groups.items(): + if len(entries) <= 1: + continue + candidates = [ + terminal + for terminal in terminals + if _terminal_endpoint_value(terminal, "QetInstanceId") == instance_id + and _terminal_endpoint_value(terminal, "QetTerminalUuid") == terminal_uuid + ] + if len(candidates) != len(entries): + continue + candidates.sort(key=lambda terminal: order.get(id(terminal), 0)) + for entry, terminal in _pair_payload_terminal_entries_with_objects(entries, candidates): + terminal_display = entry.get("terminal_display", "") + TerminalObjects.set_terminal_semantics( + terminal, + project_uuid, + entry.get("element_uuid", ""), + terminal_uuid, + instance_id, + label=terminal_display or getattr(terminal, "Label", "") or terminal_uuid, + slot_name=terminal_display or getattr(terminal, "QetTemplateSlotName", ""), + terminal_instance_id=entry.get("terminal_instance_id", ""), + ) + repaired += 1 + repaired_groups += 1 + if repaired: + try: + doc.recompute() + except Exception: + pass + return {"repaired": repaired, "groups": repaired_groups} + + def _bind_wire_task_terminals(doc, payload): """Promote matching local template terminals to QET terminal UUIDs before routing.""" report = { @@ -2115,6 +2445,9 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non opts, _document_route_constraint_options(doc), ) + connection_point_candidate_cache = opts.get("__connection_point_candidate_cache") + if not isinstance(connection_point_candidate_cache, dict): + connection_point_candidate_cache = None exit_length = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) max_exit_length = max(float(opts.get("terminal_exit_max_length", 0.0) or 0.0), 0.0) @@ -2378,6 +2711,83 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non id(candidate.get("carrier")), ) + def terminal_access_main_path_target(access_carrier): + if access_carrier is None: + return {} + kind = str(getattr(access_carrier, "QetTerminalAccessTargetKind", "") or "").strip() + name = str(getattr(access_carrier, "QetTerminalAccessTargetName", "") or "").strip() + label = str(getattr(access_carrier, "QetTerminalAccessTargetLabel", "") or "").strip() + if kind not in {"RouteCarrier", "WireDuct", "WireDuctOpenEnd", "WiringCutOut", "UserPath"}: + return {} + if not (name or label): + return {} + return {"kind": kind, "name": name, "label": label} + + def carrier_matches_terminal_access_target(carrier, target): + if carrier is None or not isinstance(target, dict) or not target: + return False + candidate_kind = str(getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or "RouteCarrier" + if target.get("kind") and candidate_kind != target.get("kind"): + return False + names = { + str(getattr(carrier, "Name", "") or "").strip(), + str(getattr(carrier, "QetRouteSourceName", "") or "").strip(), + } + labels = { + str(getattr(carrier, "Label", "") or "").strip(), + str(getattr(carrier, "QetRouteSourceLabel", "") or "").strip(), + } + target_name = str(target.get("name", "") or "").strip() + target_label = str(target.get("label", "") or "").strip() + return bool((target_name and target_name in names) or (target_label and target_label in labels)) + + def limit_candidates_to_terminal_access_target(access_carrier, candidates): + target = terminal_access_main_path_target(access_carrier) + if not target: + return candidates, False + matched = [ + candidate + for candidate in list(candidates or []) + if carrier_matches_terminal_access_target(candidate.get("carrier"), target) + ] + # 端子接入 carrier 已经记录了“应该接入哪条主路径”。能命中时直接收窄; + # 命不中则保留原候选,避免旧工程/旧路径元数据导致完全无法布线。 + if matched: + return matched, True + return candidates, False + + def connection_point_candidates_for_route_network(route_network, point, limit=0, max_distance=0.0, cacheable=True): + if connection_point_candidate_cache is None or not cacheable: + return RoutingNetwork.connection_point_candidates( + route_network, + point, + limit=limit, + max_distance=max_distance, + ) + # 批量布线里许多导线共享同一个端子或同一条线槽入口。 + # 这里缓存“端点投影到当前路径网络的候选”,不缓存最终路径,避免影响约束/避障/线道评分。 + cache_key = ( + id(route_network), + int(route_network.get("segment_count", 0) or 0) if isinstance(route_network, dict) else 0, + len((route_network.get("nodes", {}) or {})) if isinstance(route_network, dict) else 0, + _route_point_key(_vector(point)), + round(float(max_distance or 0.0), 6), + ) + cached = connection_point_candidate_cache.get(cache_key) + if cached is None: + cached = RoutingNetwork.connection_point_candidates( + route_network, + point, + limit=0, + max_distance=max_distance, + ) + connection_point_candidate_cache[cache_key] = [dict(candidate) for candidate in cached] + candidates = [dict(candidate) for candidate in cached] + max_items = max(int(limit or 0), 0) + if max_items: + return candidates[:max_items] + return candidates + def route_candidate_access_hit_count(access_point, candidate): if access_point is None or not local_access_blocked_bboxes: return 0 @@ -2457,89 +2867,120 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non if terminal_access_limit > 0.0: max_distance = min(max_distance, terminal_access_limit) if max_distance > 0.0 else terminal_access_limit candidate_limit = max(int(opts.get("network_entry_candidate_limit", 8) or 0), 1) - start_candidates = select_ranked_entry_candidates( + start_entry_candidates = connection_point_candidates_for_route_network( network, - RoutingNetwork.connection_point_candidates( - network, - start_exit, - limit=0, - max_distance=max_distance, - ), - candidate_limit, - access_point=start_exit, + start_exit, + limit=0, + max_distance=max_distance, ) - if not start_candidates: - return None - - best_route = None - best_score = None - entry_distance_cost_factor = float( - opts.get("network_entry_distance_cost_factor", 5.0) or 0.0 + limited_start_entry_candidates, start_target_limited = limit_candidates_to_terminal_access_target( + start_terminal_access_carrier, + start_entry_candidates, + ) + uses_target_limit = ( + start_target_limited + or bool(terminal_access_main_path_target(end_terminal_access_carrier)) ) - for start_rank, start_candidate in enumerate(start_candidates, start=1): - start_network = clone_route_network(network) - start_key, start_distance, start_mode = RoutingNetwork.connect_point_candidate_to_network( - start_network, - start_candidate, + + def find_best_route(entry_candidates, limit_end_to_target=True, start_target_limited=False): + start_candidate_limit = 1 if start_target_limited else candidate_limit + start_candidates = select_ranked_entry_candidates( + network, + entry_candidates, + start_candidate_limit, + access_point=start_exit, ) - if start_key is None: - continue - end_candidates = select_ranked_entry_candidates( - start_network, - RoutingNetwork.connection_point_candidates( + if not start_candidates: + return None + + best_route = None + best_score = None + entry_distance_cost_factor = float( + opts.get("network_entry_distance_cost_factor", 5.0) or 0.0 + ) + for start_rank, start_candidate in enumerate(start_candidates, start=1): + start_network = clone_route_network(network) + start_key, start_distance, start_mode = RoutingNetwork.connect_point_candidate_to_network( + start_network, + start_candidate, + ) + if start_key is None: + continue + end_entry_candidates = connection_point_candidates_for_route_network( start_network, end_exit, limit=0, max_distance=max_distance, - ), - candidate_limit, - access_point=end_exit, - ) - for end_rank, end_candidate in enumerate(end_candidates, start=1): - working_network = clone_route_network(start_network) - end_key, end_distance, end_mode = RoutingNetwork.connect_point_candidate_to_network( - working_network, - end_candidate, - ) - if end_key is None: - continue - route_data = build_route_payload( - working_network, - start_key, - end_key, - start_distance, - end_distance, - start_mode, - end_mode, - obstacle_aware=obstacle_aware, - start_candidate_rank=start_rank, - end_candidate_rank=end_rank, - ) - if route_data is None: - continue - route_score = float( - (route_data.get("route_track", {}) or {}).get("cost", 0.0) or 0.0 - ) - route_score += ( - float(start_distance or 0.0) + float(end_distance or 0.0) - ) * entry_distance_cost_factor - obstacle_hits = route_obstacle_hit_count(route_data.get("points", [])) - route_score += obstacle_hits * float( - opts.get("route_candidate_collision_penalty", 10000.0) or 0.0 + cacheable=False, ) - boundary_violations = route_boundary_violation_count(route_data.get("points", [])) - route_score += boundary_violations * float( - opts.get("route_candidate_boundary_penalty", 100000.0) or 0.0 - ) - route_data["network"]["route_candidate_obstacle_hits"] = int(obstacle_hits) - route_data["network"]["route_candidate_boundary_violations"] = int( - boundary_violations + end_target_limited = False + if limit_end_to_target: + end_entry_candidates, end_target_limited = limit_candidates_to_terminal_access_target( + end_terminal_access_carrier, + end_entry_candidates, + ) + end_candidate_limit = 1 if end_target_limited else candidate_limit + end_candidates = select_ranked_entry_candidates( + start_network, + end_entry_candidates, + end_candidate_limit, + access_point=end_exit, ) - route_data["network"]["entry_candidate_score"] = float(route_score) - if best_score is None or route_score < best_score: - best_score = route_score - best_route = route_data - return best_route + for end_rank, end_candidate in enumerate(end_candidates, start=1): + working_network = clone_route_network(start_network) + end_key, end_distance, end_mode = RoutingNetwork.connect_point_candidate_to_network( + working_network, + end_candidate, + ) + if end_key is None: + continue + route_data = build_route_payload( + working_network, + start_key, + end_key, + start_distance, + end_distance, + start_mode, + end_mode, + obstacle_aware=obstacle_aware, + start_candidate_rank=start_rank, + end_candidate_rank=end_rank, + ) + if route_data is None: + continue + route_score = float( + (route_data.get("route_track", {}) or {}).get("cost", 0.0) or 0.0 + ) + route_score += ( + float(start_distance or 0.0) + float(end_distance or 0.0) + ) * entry_distance_cost_factor + obstacle_hits = route_obstacle_hit_count(route_data.get("points", [])) + route_score += obstacle_hits * float( + opts.get("route_candidate_collision_penalty", 10000.0) or 0.0 + ) + boundary_violations = route_boundary_violation_count(route_data.get("points", [])) + route_score += boundary_violations * float( + opts.get("route_candidate_boundary_penalty", 100000.0) or 0.0 + ) + route_data["network"]["route_candidate_obstacle_hits"] = int(obstacle_hits) + route_data["network"]["route_candidate_boundary_violations"] = int( + boundary_violations + ) + route_data["network"]["entry_candidate_score"] = float(route_score) + if best_score is None or route_score < best_score: + best_score = route_score + best_route = route_data + return best_route + + route = find_best_route( + limited_start_entry_candidates, + limit_end_to_target=True, + start_target_limited=start_target_limited, + ) + if route is not None or not uses_target_limit: + return route + # 目标主路径可能来自孤立线槽开口或旧工程缓存;目标优先失败后必须退回完整网络候选。 + return find_best_route(start_entry_candidates, limit_end_to_target=False, start_target_limited=False) use_obstacle_avoidance = bool(opts.get("avoid_obstacles", True)) use_local_access_obstacle_avoidance = bool(opts.get("avoid_local_access_obstacles", True)) @@ -2704,6 +3145,24 @@ def _object_parent_chain(obj, limit=16): return chain +def _is_route_carrier_geometry(obj): + """Return True for imported solids that only visualize generated route carriers.""" + if obj is None: + return False + for parent in _object_parent_chain(obj): + parent_name = str(getattr(parent, "Name", "") or "").strip() + if parent_name == "QETWiring_02_Carriers": + # 中文说明:线槽/UserPath 的源实体可能只是 carrier 组里的显示几何, + # 不能反过来作为障碍物,否则导线沿线槽走也会被诊断为碰撞。 + return True + try: + if RoutingNetwork.is_route_carrier(parent): + return True + except Exception: + pass + return False + + def _terminal_route_endpoint_metadata(terminal): payload = { "terminal_name": str(getattr(terminal, "Name", "") or ""), @@ -2913,6 +3372,8 @@ def _obstacle_candidate_cache(doc, options=None): for obj in list(getattr(doc, "Objects", []) or []): if _has_pass_through_obstacle_semantics(obj): continue + if _is_route_carrier_geometry(obj): + continue if _is_routing_boundary(obj): continue if _is_group(obj) or _is_origin_helper(obj): @@ -3009,6 +3470,8 @@ def collect_obstacles(doc, exclude=None, options=None): continue if _has_pass_through_obstacle_semantics(obj): continue + if _is_route_carrier_geometry(obj): + continue if _is_routing_boundary(obj): continue if _is_group(obj) or _is_origin_helper(obj): @@ -3152,6 +3615,141 @@ def _route_access_points_from_payload(payload): return points +def _route_track_points_from_payload(route_track): + segments = route_track.get("segments", []) if isinstance(route_track, dict) else [] + points = [] + for segment in list(segments or []): + if not isinstance(segment, dict): + continue + if not points: + try: + points.append(_vector(segment.get("from", {}))) + except Exception: + pass + try: + points.append(_vector(segment.get("to", {}))) + except Exception: + pass + return points + + +def _route_data_with_lane(route_data, start_terminal, end_terminal, lane_index, options=None, doc=None): + if not isinstance(route_data, dict): + return route_data + opts = _merged_options(options) + rebuilt = dict(route_data) + route_track = dict(route_data.get("route_track", {}) or {}) + carrier_points = _route_track_points_from_payload(route_track) + if not carrier_points: + rebuilt["lane"] = _lane_payload(lane_index, opts, route_points=route_data.get("points", [])) + return rebuilt + + endpoint_access = route_data.get("endpoint_access", {}) if isinstance(route_data.get("endpoint_access", {}), dict) else {} + start_access_points = _route_access_points_from_payload(endpoint_access.get("start_points", [])) + end_access_points = _route_access_points_from_payload(endpoint_access.get("end_points", [])) + exit_length = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) + start_origin = start_access_points[0] if start_access_points else _terminal_origin(start_terminal) + end_origin = end_access_points[0] if end_access_points else _terminal_origin(end_terminal) + start_exit = start_access_points[-1] if start_access_points else _offset(start_origin, _terminal_direction(start_terminal), exit_length) + end_exit = end_access_points[-1] if end_access_points else _offset(end_origin, _terminal_direction(end_terminal), exit_length) + + candidate_boundaries = collect_routing_boundaries(doc, options=opts) if doc is not None else [] + candidate_obstacles = [] + if doc is not None and ( + bool(opts.get("avoid_obstacles", True)) or bool(opts.get("avoid_local_access_obstacles", True)) + ): + candidate_options = dict(opts) + candidate_options["ignore_endpoint_near_obstacles"] = False + candidate_obstacles = collect_obstacles( + doc, + exclude=[start_terminal, end_terminal], + options=candidate_options, + ) + candidate_blocked_bboxes = [ + obstacle["bbox"] + for obstacle in candidate_obstacles + if obstacle.get("bbox") + ] + local_access_blocked_bboxes = [ + obstacle["bbox"] + for obstacle in candidate_obstacles + if obstacle.get("bbox") and _is_local_access_obstacle(obstacle) + ] + route_candidate_blocked_bboxes = ( + candidate_blocked_bboxes + if bool(opts.get("avoid_obstacles", True)) + else local_access_blocked_bboxes + ) + scan_margin = max( + float(opts.get("local_access_obstacle_scan_margin", 0.0) or 0.0), + LOCAL_ACCESS_DETOUR_CLEARANCE, + float(opts.get("obstacle_clearance", 0.0) or 0.0), + ) + + def local_access_obstacle_bboxes(start_point, end_point, preferred_axis=None): + return _local_access_obstacle_bboxes( + start_point, + end_point, + local_access_blocked_bboxes, + preferred_axis=preferred_axis, + margin=scan_margin, + ) + + def route_boundary_violation_count(points): + if not candidate_boundaries: + return 0 + return sum(1 for point in points or [] if not _point_inside_any_boundary(point, candidate_boundaries)) + + def route_obstacle_hit_count(points): + hits = 0 + if not route_candidate_blocked_bboxes: + return hits + for index in range(max(len(points or []) - 1, 0)): + start = points[index] + end = points[index + 1] + for bbox in _filter_obstacle_bboxes_near_polyline( + [start, end], + route_candidate_blocked_bboxes, + margin=scan_margin, + ): + if _segment_intersects_bbox(start, end, bbox): + hits += 1 + break + return hits + + lane = _lane_payload_boundary_aware( + lane_index, + opts, + route_points=carrier_points, + boundary_violation_count=route_boundary_violation_count, + obstacle_hit_count=route_obstacle_hit_count, + ) + shifted_carrier_points = _apply_lane_offset(carrier_points, lane) + points = [] + for point in start_access_points or [start_origin, start_exit]: + _append_unique(points, point) + _append_orthogonal( + points, + shifted_carrier_points[0], + obstacle_bboxes=local_access_obstacle_bboxes(points[-1], shifted_carrier_points[0]), + ) + for point in shifted_carrier_points[1:]: + _append_unique(points, point) + _append_orthogonal( + points, + end_exit, + obstacle_bboxes=local_access_obstacle_bboxes(points[-1], end_exit), + ) + for point in reversed(end_access_points or [end_origin, end_exit]): + _append_unique(points, point) + preserved = {_route_point_key(point) for point in shifted_carrier_points} + for access_point in list(start_access_points or []) + list(end_access_points or []): + preserved.add(_route_point_key(access_point)) + rebuilt["points"] = _simplify_collinear_points(points, preserved_point_keys=preserved) + rebuilt["lane"] = lane + return rebuilt + + def _access_collision_segment_indices(points, access_points, from_start=True): route_points = [_vector(point) for point in points or []] access_points = [_vector(point) for point in access_points or []] @@ -3397,6 +3995,41 @@ def _wire_style_draw_style(wire_style): return "Solid" +def _set_wire_style_application_metadata(wire, wire_style, line_width, line_color, draw_style): + applied = bool(isinstance(wire_style, dict) and wire_style) + _set_bool( + wire, + "QetWireStyleApplied", + applied, + "Whether the QET wire style has been applied to the visible 3D wire", + ) + _set_string( + wire, + "QetAppliedWireLineColor", + str((wire_style or {}).get("line_color", "") or ""), + "Applied QET wire line color text", + ) + _set_string( + wire, + "QetAppliedWireLineWidth", + str(line_width), + "Applied QET wire line width", + ) + _set_string( + wire, + "QetAppliedWireDrawStyle", + str(draw_style or "Solid"), + "Applied QET wire draw style", + ) + if line_color is not None: + _set_string( + wire, + "QetAppliedWireLineColorRgb", + ",".join(str(value) for value in line_color), + "Applied QET wire RGB color", + ) + + def _resolve_wire_style_from_database(wire_style_id, database_path="", project_uuid=""): style_id = str(wire_style_id or "").strip() db_path = str(database_path or "").strip() @@ -3491,17 +4124,200 @@ def resolve_wire_style(wire_style_id, options=None, project_uuid=""): def _style_wire(wire, collision_count=0, wire_style=None): + line_width = _wire_style_line_width(wire_style) or 5.0 + draw_style = _wire_style_draw_style(wire_style) + line_color = (1.0, 0.1, 0.0) if collision_count else (_wire_style_color(wire_style) or (0.0, 0.35, 1.0)) try: wire.ViewObject.Visibility = True - wire.ViewObject.LineWidth = _wire_style_line_width(wire_style) or 5.0 + wire.ViewObject.LineWidth = line_width if hasattr(wire.ViewObject, "DrawStyle"): - wire.ViewObject.DrawStyle = _wire_style_draw_style(wire_style) + wire.ViewObject.DrawStyle = draw_style if hasattr(wire.ViewObject, "DisplayMode"): wire.ViewObject.DisplayMode = "Wireframe" - if collision_count: - wire.ViewObject.LineColor = (1.0, 0.1, 0.0) - else: - wire.ViewObject.LineColor = _wire_style_color(wire_style) or (0.0, 0.35, 1.0) + wire.ViewObject.LineColor = line_color + if hasattr(wire.ViewObject, "ShapeColor"): + wire.ViewObject.ShapeColor = line_color + except Exception: + pass + _set_wire_style_application_metadata(wire, wire_style, line_width, line_color, draw_style) + + +def _wire_style_from_routed_wire(wire): + text = str(getattr(wire, "QetWireStyleJson", "") or "").strip() + if not text: + return {} + try: + payload = json.loads(text) + except Exception: + return {} + return payload if isinstance(payload, dict) else {} + + +def _routed_wire_has_collision_warning(wire): + codes = str(getattr(wire, "QetRouteIssueCodes", "") or "").lower() + return "collision" in codes + + +def _ensure_routed_wires_visible_and_styled(doc): + if doc is None: + return 0 + shown = 0 + for wire in list(WiringObjects.iter_routed_wire_objects(doc)): + if (getattr(wire, "RouteType", "") or "").strip() != "RoutedConnection": + continue + _style_wire( + wire, + collision_count=1 if _routed_wire_has_collision_warning(wire) else 0, + wire_style=_wire_style_from_routed_wire(wire), + ) + shown += 1 + routed_group = doc.getObject("QETWiring_04_Routed") + if routed_group is not None: + try: + routed_group.ViewObject.Visibility = True + except Exception: + pass + return shown + + +def apply_phase1_acceptance_view(doc): + """整理一阶段验收视图:隐藏辅助路径对象,显示并刷新最终导线。 + + 这个入口不重新生成路径、不删除对象、不写数据库,主要用于打开旧工程后把 + route carrier 调试网格收起,恢复“只看最终导线”的手动验收状态。 + """ + hidden_carriers = RoutingNetwork.set_route_carriers_visibility(doc, False) + shown_wires = _ensure_routed_wires_visible_and_styled(doc) + try: + if doc is not None: + doc.recompute() + except Exception: + pass + try: + if Gui is not None: + Gui.updateGui() + if hasattr(Gui, "SendMsgToActiveView"): + Gui.SendMsgToActiveView("ViewFit") + except Exception: + pass + return { + "hidden_route_carriers": hidden_carriers, + "shown_routed_wires": shown_wires, + "routed_wire_visibility": _routed_wire_visibility_summary(doc), + "route_carrier_visibility": _route_carrier_visibility_summary(doc, expected_hidden=True), + "wire_style_application": _wire_style_application_summary(doc), + } + + +def _object_visibility(obj): + try: + return bool(obj.ViewObject.Visibility) + except Exception: + return None + + +def _route_object_sample(obj): + return { + "name": str(getattr(obj, "Name", "") or ""), + "label": str(getattr(obj, "Label", "") or ""), + "kind": str(getattr(obj, "QetRouteCarrierKind", "") or ""), + } + + +def _routed_wire_visibility_summary(doc, limit=8): + routed = [] + for wire in list(WiringObjects.iter_routed_wire_objects(doc)): + if (getattr(wire, "RouteType", "") or "").strip() == "RoutedConnection": + routed.append(wire) + hidden = [wire for wire in routed if not _object_visibility(wire)] + unknown = [wire for wire in routed if _object_visibility(wire) is None] + hidden = [wire for wire in routed if _object_visibility(wire) is False] + group_visible = False + try: + routed_group = doc.getObject("QETWiring_04_Routed") if doc is not None else None + group_visible = _object_visibility(routed_group) if routed_group is not None else None + except Exception: + group_visible = None + return { + "routed": len(routed), + "visible": len([wire for wire in routed if _object_visibility(wire) is True]), + "hidden": len(hidden), + "unknown_visibility": len(unknown), + "group_visible": group_visible, + "hidden_samples": [_route_object_sample(wire) for wire in hidden[:limit]], + "unknown_visibility_samples": [_route_object_sample(wire) for wire in unknown[:limit]], + } + + +def _wire_style_application_summary(doc, limit=8): + routed = [ + wire + for wire in list(WiringObjects.iter_routed_wire_objects(doc)) + if (getattr(wire, "RouteType", "") or "").strip() == "RoutedConnection" + ] + expected = [] + applied = [] + missing = [] + styled_black = [] + for wire in routed: + style_id = str(getattr(wire, "QetWireStyleId", "") or "").strip() + style_json = str(getattr(wire, "QetWireStyleJson", "") or "").strip() + expects_style = bool(style_id or style_json) + is_applied = bool(getattr(wire, "QetWireStyleApplied", False)) + if expects_style: + expected.append(wire) + if is_applied: + applied.append(wire) + else: + missing.append(wire) + rgb_text = str(getattr(wire, "QetAppliedWireLineColorRgb", "") or "").strip() + # 这里用于回答“黑色线是本来黑色还是未渲染”:有应用元数据且 RGB 为 0,0,0 才算样式黑色。 + if is_applied and rgb_text in {"0,0,0", "0.0,0.0,0.0"}: + styled_black.append(wire) + return { + "routed": len(routed), + "expected": len(expected), + "applied": len(applied), + "missing_application": len(missing), + "styled_black": len(styled_black), + "missing_application_samples": [_route_object_sample(wire) for wire in missing[:limit]], + } + + +def _route_carrier_visibility_summary(doc, expected_hidden=True, limit=8): + carriers = list(RoutingNetwork.collect_route_carriers(doc)) + visible = [carrier for carrier in carriers if _object_visibility(carrier) is True] + unknown = [carrier for carrier in carriers if _object_visibility(carrier) is None] + kind_counts = {} + visible_kind_counts = {} + for carrier in carriers: + kind = str(getattr(carrier, "QetRouteCarrierKind", "") or "RouteCarrier") + kind_counts[kind] = kind_counts.get(kind, 0) + 1 + for carrier in visible: + kind = str(getattr(carrier, "QetRouteCarrierKind", "") or "RouteCarrier") + visible_kind_counts[kind] = visible_kind_counts.get(kind, 0) + 1 + return { + "expected_hidden": bool(expected_hidden), + "total": len(carriers), + "visible_after_hide": len(visible), + "unknown_visibility": len(unknown), + "kind_counts": dict(sorted(kind_counts.items())), + "visible_kind_counts": dict(sorted(visible_kind_counts.items())), + "visible_samples": [_route_object_sample(carrier) for carrier in visible[:limit]], + "unknown_visibility_samples": [_route_object_sample(carrier) for carrier in unknown[:limit]], + } + + +def _refresh_routing_view(doc): + if Gui is None: + return + try: + if getattr(App, "ActiveDocument", None) is doc: + Gui.updateGui() + except Exception: + pass + try: + Gui.SendMsgToActiveView("ViewFit") except Exception: pass @@ -3563,13 +4379,17 @@ def route_eplan_connection_between_terminals( raise AutoRoutingError("Project UUID is required for routing connections.") wire_style = resolve_wire_style(effective_wire_style_id, options=opts, project_uuid=project_uuid) - route_data = build_network_route( - start_terminal, - end_terminal, - route_index=route_index, - options=opts, - doc=doc, - ) + route_data_override = opts.get("__route_data_override") + if isinstance(route_data_override, dict): + route_data = dict(route_data_override) + else: + route_data = build_network_route( + start_terminal, + end_terminal, + route_index=route_index, + options=opts, + doc=doc, + ) if route_data is None: if _has_route_constraints(opts) or _has_route_constraints( _document_route_constraint_options(doc) @@ -4182,7 +5002,7 @@ def _context_wire_style_database_path(project_uuid="", style_ids=None, exclude_p ) -def _apply_wire_style_database_option(opts, payload): +def _apply_wire_style_database_option(opts, payload, doc=None): if not isinstance(opts, dict): return opts project_uuid = "" @@ -4205,6 +5025,13 @@ def _apply_wire_style_database_option(opts, payload): style_ids=style_ids, exclude_paths=[configured_path], ) + if not fallback_path: + fallback_path = _document_wire_style_database_path( + doc, + project_uuid=project_uuid, + style_ids=style_ids, + exclude_paths=[configured_path], + ) if fallback_path: opts["wire_style_database_fallback_from"] = configured_path opts["wire_style_database_path"] = fallback_path @@ -4212,6 +5039,14 @@ def _apply_wire_style_database_option(opts, payload): context_db_path = _context_wire_style_database_path(project_uuid=project_uuid, style_ids=style_ids) if context_db_path and not str(opts.get("wire_style_database_path", "") or "").strip(): opts["wire_style_database_path"] = context_db_path + if not str(opts.get("wire_style_database_path", "") or "").strip(): + document_db_path = _document_wire_style_database_path( + doc, + project_uuid=project_uuid, + style_ids=style_ids, + ) + if document_db_path: + opts["wire_style_database_path"] = document_db_path return opts @@ -4233,6 +5068,35 @@ def _context_exchange_json_path(): return os.environ.get("QET_2D_TO_3D_JSON", "").strip() +def _document_exchange_json_path(doc): + filename = str(getattr(doc, "FileName", "") or "").strip() + if not filename: + return "" + try: + directory = os.path.dirname(os.path.abspath(filename)) + except Exception: + return "" + if not directory: + return "" + if os.path.basename(directory).lower() == ".qet_freecad": + json_path = os.path.join(directory, "2d_to_3d.json") + if os.path.exists(json_path): + return json_path + return filename + + +def _document_wire_style_database_path(doc, project_uuid="", style_ids=None, exclude_paths=None): + json_path = _document_exchange_json_path(doc) + if not json_path: + return "" + return _discover_wire_style_database_path_from_json_path( + json_path, + project_uuid=project_uuid, + style_ids=style_ids, + exclude_paths=exclude_paths, + ) + + def _load_context_exchange_payload(): json_path = _context_exchange_json_path() if not json_path: @@ -4245,6 +5109,18 @@ def _load_context_exchange_payload(): return payload if isinstance(payload, dict) else {} +def _load_document_exchange_payload(doc): + json_path = _document_exchange_json_path(doc) + if not json_path or os.path.splitext(json_path)[1].lower() != ".json": + return {} + try: + with open(json_path, "r", encoding="utf-8") as handle: + payload = json.load(handle) + except Exception: + return {} + return payload if isinstance(payload, dict) else {} + + def _context_payload_matches_project(payload, context_payload): payload_project_uuid = "" if isinstance(payload, dict): @@ -4293,6 +5169,24 @@ def _load_context_payload_with_devices(payload): return merged +def _load_document_payload_with_devices(doc, payload): + if not isinstance(payload, dict): + return payload + if isinstance(payload.get("devices"), list) and payload.get("devices"): + return payload + json_path = _document_exchange_json_path(doc) + document_payload = _load_document_exchange_payload(doc) + devices = list(document_payload.get("devices", []) or []) if isinstance(document_payload, dict) else [] + if not devices or not _context_payload_matches_project(payload, document_payload): + return payload + # 只补设备列表,保留当前 FCStd 任务导线,避免用磁盘 JSON 覆盖用户正在测试的任务对象。 + merged = dict(payload) + merged["devices"] = devices + merged["__context_devices_json_path"] = json_path + merged["__context_device_count"] = len(devices) + return merged + + def _preflight_wire_payload(doc, payload): doc_project_uuid = _project_uuid(doc) payload_project_uuid = "" @@ -4303,9 +5197,12 @@ def _preflight_wire_payload(doc, payload): return task_payload, list(task_payload.get("wires") or []), "tasks" payload = _load_context_payload_with_wire_styles(payload) payload = _load_context_payload_with_devices(payload) + payload = _load_document_payload_with_devices(doc, payload) if isinstance(payload, dict) and isinstance(payload.get("wires"), list): return payload, list(payload.get("wires") or []), "payload" task_payload = _wire_tasks_payload(doc) + task_payload = _load_context_payload_with_devices(task_payload) + task_payload = _load_document_payload_with_devices(doc, task_payload) return task_payload, list(task_payload.get("wires") or []), "tasks" @@ -4483,13 +5380,28 @@ def _preflight_routeability_summary(doc, wires, terminals, options=None): } if sample_limit <= 0: return summary + # terminal_uuid 在当前 v2 快照里可能重复;预检抽样必须和正式布线一样, + # 优先按设备实例、2D 元件和端子显示名消歧,避免把 B1 误判成同 UUID 的 A1。 + terminal_candidates = list(opts.get("__terminal_candidates", []) or []) for item in wires or []: if not isinstance(item, dict): continue start_uuid = _wire_item_value(item, "start_terminal_uuid") end_uuid = _wire_item_value(item, "end_terminal_uuid") - start_terminal = terminals.get(start_uuid) - end_terminal = terminals.get(end_uuid) + if terminal_candidates: + start_terminal = _terminal_endpoint_match( + terminal_candidates, + item, + "start", + ).get("terminal") + end_terminal = _terminal_endpoint_match( + terminal_candidates, + item, + "end", + ).get("terminal") + else: + start_terminal = terminals.get(start_uuid) + end_terminal = terminals.get(end_uuid) if start_terminal is None or end_terminal is None: continue summary["eligible_wires"] += 1 @@ -4539,17 +5451,24 @@ def preflight_eplan_connections(doc, payload=None, options=None): if doc is None: raise AutoRoutingError("No FreeCAD document is available.") opts = _merged_options(options) + opts.setdefault("__route_network_cache", {}) source_payload, wires, source = _preflight_wire_payload(doc, payload) - _apply_wire_style_database_option(opts, source_payload) + _apply_wire_style_database_option(opts, source_payload, doc=doc) opts.setdefault("__wire_style_cache", {}) + opts.setdefault("__connection_point_candidate_cache", {}) project_uuid = str(source_payload.get("project_uuid", "") or _project_uuid(doc)).strip() + terminal_metadata_repair = _repair_duplicate_terminal_metadata_from_payload(doc, source_payload) terminals = index_terminals(doc) terminal_candidates = _collect_routable_terminals(doc) duplicate_terminal_summary = _terminal_uuid_duplicate_summary(terminal_candidates) + payload_terminal_instance_duplicates = _payload_terminal_instance_duplicate_summary(source_payload) + unreferenced_payload_terminals = _payload_unreferenced_terminal_summary(source_payload) local_terminal_count = sum( 1 - for terminal_uuid in terminals - if TerminalObjects.is_local_terminal_uuid(terminal_uuid) + for terminal in terminal_candidates + if TerminalObjects.is_local_terminal_uuid( + _terminal_endpoint_value(terminal, "QetTerminalUuid") + ) ) report = { "ok": True, @@ -4557,11 +5476,26 @@ def preflight_eplan_connections(doc, payload=None, options=None): "runtime_version": AUTO_ROUTING_RUNTIME_VERSION, "project_uuid": project_uuid, "total_wires": len(wires), - "available_terminals": len(terminals), + "available_terminals": len(terminal_candidates), "available_terminal_objects": len(terminal_candidates), + "unique_terminal_uuids": len(terminals), "local_terminals": local_terminal_count, + "repaired_duplicate_terminal_metadata": terminal_metadata_repair.get("repaired", 0), + "repaired_duplicate_terminal_metadata_groups": terminal_metadata_repair.get("groups", 0), "duplicate_terminal_uuid_count": duplicate_terminal_summary["duplicate_terminal_uuid_count"], "duplicate_terminal_uuid_samples": duplicate_terminal_summary["duplicate_terminal_uuid_samples"], + "duplicate_payload_terminal_instance_id_count": payload_terminal_instance_duplicates[ + "duplicate_payload_terminal_instance_id_count" + ], + "duplicate_payload_terminal_instance_id_samples": payload_terminal_instance_duplicates[ + "duplicate_payload_terminal_instance_id_samples" + ], + "unreferenced_payload_terminal_count": unreferenced_payload_terminals[ + "unreferenced_payload_terminal_count" + ], + "unreferenced_payload_terminal_samples": unreferenced_payload_terminals[ + "unreferenced_payload_terminal_samples" + ], "route_network_carriers": 0, "route_network_segments": 0, "route_network_nodes": 0, @@ -4657,6 +5591,34 @@ def preflight_eplan_connections(doc, payload=None, options=None): severity="warning", ) + if _safe_int(report.get("duplicate_terminal_uuid_count", 0)) > 0: + _append_preflight_issue( + report, + "duplicate_3d_terminal_uuids", + "3D工程端子 UUID 重复;FreeCAD 会优先按设备实例、2D 设备 UUID 和端子显示名消歧,缺少这些上下文时可能无法稳定匹配。", + severity="warning", + count=_safe_int(report.get("duplicate_terminal_uuid_count", 0)), + samples=report.get("duplicate_terminal_uuid_samples", []), + ) + if _safe_int(report.get("duplicate_payload_terminal_instance_id_count", 0)) > 0: + _append_preflight_issue( + report, + "duplicate_payload_terminal_instance_ids", + "2d_to_3d.json 中 terminal_instance_id 存在重复,FreeCAD 会临时生成稳定 3D 端子实例 ID 消歧。", + severity="warning", + count=_safe_int(report.get("duplicate_payload_terminal_instance_id_count", 0)), + samples=report.get("duplicate_payload_terminal_instance_id_samples", []), + ) + if _safe_int(report.get("unreferenced_payload_terminal_count", 0)) > 0: + _append_preflight_issue( + report, + "payload_terminals_without_wires", + "2d_to_3d.json 中存在没有被任何 wires[] 端点引用的设备端子;这可能是未接线端子,也可能是 QET 少导出了导线任务。", + severity="warning", + count=_safe_int(report.get("unreferenced_payload_terminal_count", 0)), + samples=report.get("unreferenced_payload_terminal_samples", []), + ) + missing_endpoint_uuids = set() for item in wires: if not isinstance(item, dict): @@ -4705,7 +5667,9 @@ def preflight_eplan_connections(doc, payload=None, options=None): report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) if report["route_network_segments"] > 0: - routeability = _preflight_routeability_summary(doc, wires, terminals, options=opts) + routeability_options = dict(opts) + routeability_options["__terminal_candidates"] = terminal_candidates + routeability = _preflight_routeability_summary(doc, wires, terminals, options=routeability_options) report["routeability_checked"] = int(routeability.get("checked", 0) or 0) report["routeability_sample_limit"] = int(routeability.get("sample_limit", 0) or 0) report["routeability_eligible_wires"] = int(routeability.get("eligible_wires", 0) or 0) @@ -4932,6 +5896,27 @@ def format_eplan_routing_preflight_report(report): parts.append("{0} {1} 条".format(label, value)) if parts: message += "\n导线样式:{0}。".format(",".join(parts)) + duplicate_terminal_uuid_count = _safe_int(report.get("duplicate_terminal_uuid_count", 0)) + if duplicate_terminal_uuid_count > 0: + message += "\n3D工程端子 UUID 重复:{0} 组".format(duplicate_terminal_uuid_count) + samples = [item for item in list(report.get("duplicate_terminal_uuid_samples", []) or []) if isinstance(item, dict)] + if samples: + sample = samples[0] + message += ",示例 {0} 出现 {1} 次".format( + sample.get("terminal_uuid", "未知端子"), + _safe_int(sample.get("count", 0)), + ) + message += ";需要依赖设备实例、2D 设备 UUID 或端子显示名消歧。" + unreferenced_count = _safe_int(report.get("unreferenced_payload_terminal_count", 0)) + if unreferenced_count > 0: + message += "\n未被 wires[] 引用的端子:{0} 个".format(unreferenced_count) + samples = [item for item in list(report.get("unreferenced_payload_terminal_samples", []) or []) if isinstance(item, dict)] + if samples: + sample = samples[0] + device_text = str(sample.get("device_label", "") or sample.get("device_instance_id", "") or "未知设备") + terminal_text = str(sample.get("terminal_display", "") or sample.get("terminal_uuid", "") or "未知端子") + message += ",示例 {0}/{1}".format(device_text, terminal_text) + message += "。" issues = [item for item in list(report.get("issues", []) or []) if isinstance(item, dict)] if issues: message += "\n预检问题:{0}。".format( @@ -4987,13 +5972,17 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la payload = _load_context_payload_with_wire_styles(payload) payload = _load_context_payload_with_devices(payload) + payload = _load_document_payload_with_devices(doc, payload) opts = _merged_options(options) - _apply_wire_style_database_option(opts, payload) + _apply_wire_style_database_option(opts, payload, doc=doc) opts.setdefault("__wire_style_cache", {}) + opts.setdefault("__connection_point_candidate_cache", {}) + terminal_metadata_repair = _repair_duplicate_terminal_metadata_from_payload(doc, payload) terminal_binding_report = bind_wire_task_terminals_from_payload(doc, payload) terminals = index_terminals(doc) terminal_candidates = _collect_routable_terminals(doc) duplicate_terminal_summary = _terminal_uuid_duplicate_summary(terminal_candidates) + payload_terminal_instance_duplicates = _payload_terminal_instance_duplicate_summary(payload) local_terminal_count = sum( 1 for terminal in terminal_candidates @@ -5008,11 +5997,20 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "project_uuid": project_uuid_value, "runtime_version": AUTO_ROUTING_RUNTIME_VERSION, "total_wires": len(wires), - "available_terminals": len(terminals), + "available_terminals": len(terminal_candidates), "available_terminal_objects": len(terminal_candidates), + "unique_terminal_uuids": len(terminals), "local_terminals": local_terminal_count, + "repaired_duplicate_terminal_metadata": terminal_metadata_repair.get("repaired", 0), + "repaired_duplicate_terminal_metadata_groups": terminal_metadata_repair.get("groups", 0), "duplicate_terminal_uuid_count": duplicate_terminal_summary["duplicate_terminal_uuid_count"], "duplicate_terminal_uuid_samples": duplicate_terminal_summary["duplicate_terminal_uuid_samples"], + "duplicate_payload_terminal_instance_id_count": payload_terminal_instance_duplicates[ + "duplicate_payload_terminal_instance_id_count" + ], + "duplicate_payload_terminal_instance_id_samples": payload_terminal_instance_duplicates[ + "duplicate_payload_terminal_instance_id_samples" + ], "auto_bound_terminals": terminal_binding_report["bound"], "auto_created_terminals": terminal_binding_report["created"], "auto_terminal_binding_warnings": terminal_binding_report["warnings"], @@ -5071,14 +6069,25 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } if isinstance(prepared_layout, dict): report["prepared_layout"] = prepared_layout - try: - route_network = RoutingNetwork.build_route_graph( - doc, - adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), - ) - except Exception as exc: - route_network = {} - report["route_network_error"] = str(exc) + route_network = {} + route_network_reused = False + cache = _route_network_cache(opts) + cached_route_network = cache.get("route_network") if cache is not None else None + if isinstance(cached_route_network, dict) and int(cached_route_network.get("segment_count", 0) or 0) > 0: + route_network = cached_route_network + route_network_reused = True + else: + try: + route_network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + if cache is not None and int(route_network.get("segment_count", 0) or 0) > 0: + cache["route_network"] = route_network + except Exception as exc: + route_network = {} + report["route_network_error"] = str(exc) + report["route_network_reused"] = bool(route_network_reused) report["route_network_carriers"] = int(route_network.get("carrier_count", 0) or 0) report["route_network_segments"] = int(route_network.get("segment_count", 0) or 0) report["route_network_nodes"] = len(route_network.get("nodes", {}) or {}) @@ -5169,6 +6178,8 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la route_options["avoid_obstacles"] = bool(item.get("__avoid_obstacles_override")) if isinstance(item, dict) and "__replace_existing_override" in item: route_options["replace_existing"] = bool(item.get("__replace_existing_override")) + if isinstance(item, dict) and "__route_data_override" in item: + route_options["__route_data_override"] = item.get("__route_data_override") if isinstance(route_network, dict) and route_network.get("segment_count", 0) > 0: route_options["__base_route_network"] = route_network route_options["__obstacle_candidate_cache"] = obstacle_candidate_cache @@ -5327,10 +6338,24 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la final_lane_index = max(route_lane_index, shared_lane_index) if final_lane_index != route_lane_index: initial_wire = result.get("wire") if isinstance(result, dict) else None + lane_route_data = _route_data_with_lane( + result, + start_terminal, + end_terminal, + final_lane_index, + options=opts, + doc=doc, + ) + if initial_wire is not None: + _remove_routing_connection_objects(doc, [initial_wire]) try: result = create_route( final_lane_index, - dict(item, __segment_usage_costs=segment_usage_costs), + dict( + item, + __segment_usage_costs=segment_usage_costs, + __route_data_override=lane_route_data, + ), start_terminal, end_terminal, endpoint_metadata, @@ -6050,6 +7075,15 @@ def _route_capacity_pressure_samples(report, limit=8): return samples +def _show_candidate_debug_warnings(report): + if not isinstance(report, dict): + return False + return bool( + report.get("show_candidate_debug_warnings") + or report.get("show_route_debug_warnings") + ) + + _ROUTE_QUALITY_WARNING_KIND_LABELS = { "RoutingRange": "布线面", "AuxiliaryPath": "辅助路径", @@ -7386,6 +8420,26 @@ def format_eplan_connection_route_report(report): created_count = _safe_int(auto_bridges.get("created_count", 0)) if created_count > 0: message += "\n自动诊断桥接:生成 UserPath {0} 条。".format(created_count) + unconnected_targets = _safe_int( + auto_bridges.get("unconnected_terminal_access_bridge_targets", 0) + ) + unconnected_created = _safe_int( + auto_bridges.get("unconnected_terminal_access_user_path_bridges", 0) + ) + if unconnected_targets > 0 or unconnected_created > 0: + message += " 未接入端子接入段 {0} 个,生成 {1} 条。".format( + unconnected_targets, + unconnected_created, + ) + pair_labels = [ + str(label or "").strip() + for label in list( + auto_bridges.get("unconnected_terminal_access_bridge_pair_labels", []) or [] + ) + if str(label or "").strip() + ] + if pair_labels: + message += " 配对:{0}。".format("、".join(pair_labels[:3])) auto_detour_bridges = report.get("auto_main_path_detour_bridges", {}) if isinstance(auto_detour_bridges, dict): created_count = _safe_int(auto_detour_bridges.get("created_count", 0)) @@ -7480,6 +8534,35 @@ def format_eplan_connection_route_report(report): hidden_route_carriers = int(report.get("hidden_route_carriers", 0) or 0) if hidden_route_carriers > 0: message += "\n已隐藏走线路径辅助对象:{0} 条。".format(hidden_route_carriers) + duplicate_payload_terminal_instances = _safe_int( + report.get("duplicate_payload_terminal_instance_id_count", 0) + ) + if duplicate_payload_terminal_instances > 0: + message += "\n输入端子实例ID重复:{0} 组,已按设备实例/端子显示名在 FreeCAD 侧临时消歧。".format( + duplicate_payload_terminal_instances + ) + wire_visibility = report.get("routed_wire_visibility", {}) + if isinstance(wire_visibility, dict): + hidden_wires = _safe_int(wire_visibility.get("hidden", 0)) + if hidden_wires > 0: + message += "\n导线可见性异常:{0} 条 RoutedConnection 仍不可见。".format(hidden_wires) + style_application = report.get("wire_style_application", {}) + if isinstance(style_application, dict): + missing_application = _safe_int(style_application.get("missing_application", 0)) + styled_black = _safe_int(style_application.get("styled_black", 0)) + if styled_black > 0: + message += "\n黑色导线:{0} 条来自 wire_properties 样式,属于已解析并已应用的黑色线。".format( + styled_black + ) + if missing_application > 0: + message += "\n导线样式实际应用异常:{0} 条导线有样式 ID/样式数据但未渲染到 ViewObject。".format( + missing_application + ) + carrier_visibility = report.get("route_carrier_visibility", {}) + if isinstance(carrier_visibility, dict) and bool(carrier_visibility.get("expected_hidden")): + visible_carriers = _safe_int(carrier_visibility.get("visible_after_hide", 0)) + if visible_carriers > 0: + message += "\n辅助路径显示异常:{0} 条 route carrier 仍可见。".format(visible_carriers) bridged_segments = _route_network_metric_max(report, "bridged_segments") blocked_segments = _route_network_metric_max(report, "blocked_segments") network_parts = [] @@ -7499,7 +8582,7 @@ def format_eplan_connection_route_report(report): if max_offset > 0.0: lane_text += ",最大偏移 {0:.1f} mm".format(max_offset) message += "\n{0}。".format(lane_text) - capacity_pressure = _route_capacity_pressure_summary(report) + capacity_pressure = _route_capacity_pressure_summary(report) if _show_candidate_debug_warnings(report) else {} if capacity_pressure: message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}。".format( capacity_pressure.get("max_parallel_wires", 0), @@ -7595,7 +8678,11 @@ def format_eplan_connection_route_report(report): ",".join(sample.get("warning_parts", []) or []), route_text, ) - obstacle_entry_warning = _route_candidate_obstacle_warning_summary(report) + obstacle_entry_warning = ( + _route_candidate_obstacle_warning_summary(report) + if _show_candidate_debug_warnings(report) + else {} + ) if obstacle_entry_warning: sample = obstacle_entry_warning.get("sample", {}) route_text = "" @@ -7835,6 +8922,30 @@ def _compact_routing_preflight_report(report, sample_limit=8): missing_samples = list(report.get("missing_endpoint_samples", []) or []) payload["missing_endpoint_samples"] = missing_samples[:limit] payload["missing_endpoint_samples_count"] = len(missing_samples) + payload["duplicate_terminal_uuid_count"] = _safe_int( + report.get("duplicate_terminal_uuid_count", 0) + ) + duplicate_terminal_uuid_samples = list(report.get("duplicate_terminal_uuid_samples", []) or []) + payload["duplicate_terminal_uuid_samples"] = duplicate_terminal_uuid_samples[:limit] + payload["duplicate_terminal_uuid_samples_count"] = len(duplicate_terminal_uuid_samples) + payload["duplicate_payload_terminal_instance_id_count"] = _safe_int( + report.get("duplicate_payload_terminal_instance_id_count", 0) + ) + duplicate_payload_terminal_instance_id_samples = list( + report.get("duplicate_payload_terminal_instance_id_samples", []) or [] + ) + payload["duplicate_payload_terminal_instance_id_samples"] = ( + duplicate_payload_terminal_instance_id_samples[:limit] + ) + payload["duplicate_payload_terminal_instance_id_samples_count"] = len( + duplicate_payload_terminal_instance_id_samples + ) + payload["unreferenced_payload_terminal_count"] = _safe_int( + report.get("unreferenced_payload_terminal_count", 0) + ) + unreferenced_samples = list(report.get("unreferenced_payload_terminal_samples", []) or []) + payload["unreferenced_payload_terminal_samples"] = unreferenced_samples[:limit] + payload["unreferenced_payload_terminal_samples_count"] = len(unreferenced_samples) unrouteable_samples = list(report.get("unrouteable_samples", []) or []) payload["unrouteable_samples"] = unrouteable_samples[:limit] payload["unrouteable_samples_count"] = len(unrouteable_samples) @@ -7850,6 +8961,10 @@ def _compact_routing_preflight_report(report, sample_limit=8): value = report.get(key) if isinstance(value, dict): payload[key] = dict(value) + payload["issue_labels"] = [ + _routing_diagnostic_issue_label(code) + for code in payload.get("issue_codes", []) + ] payload["diagnostic_payload"] = "compact-routing-preflight-v1" return payload @@ -7973,6 +9088,9 @@ _ROUTING_DIAGNOSTIC_ISSUE_LABELS = { "route_candidate_obstacle_hits": "候选路径碰撞风险", "route_candidate_boundary_violations": "候选路径越出柜内边界", "route_capacity_pressure": "路径容量压力", + "routed_wires_not_visible": "导线生成后不可见", + "wire_styles_not_applied": "导线样式未实际应用", + "route_carriers_still_visible": "辅助路径对象仍可见", "diagnostic_json_empty": "诊断 JSON 为空", "diagnostic_json_invalid": "诊断 JSON 无效", "routed_wire_diagnostics_missing": "导线诊断缺失", @@ -7982,6 +9100,9 @@ _ROUTING_DIAGNOSTIC_ISSUE_LABELS = { "no_3d_terminals_for_element": "设备缺少工程端子", "no_3d_terminals_for_instance": "实例缺少工程端子", "terminal_uuid_not_in_element": "端子UUID不匹配", + "duplicate_3d_terminal_uuids": "3D端子UUID重复", + "duplicate_payload_terminal_instance_ids": "输入端子实例ID重复", + "payload_terminals_without_wires": "输入端子未被导线引用", } @@ -8437,6 +9558,24 @@ def _routing_diagnostic_recommended_actions(summary): add("按端子接入退回布线面示例定位设备侧缺口,再重新生成布线路径网络") if "unconnected_terminals" in issue_codes: add("点击“选择未接入端子”定位未接入路由网络或接入距离超限的端子") + diagnostics = summary.get("diagnostics", {}) or {} + path_payload = ((diagnostics.get("RoutingPathNetwork", {}) or {}).get("payload", {}) or {}) + unconnected_samples = ( + list(path_payload.get("unconnected_terminals", []) or []) + if isinstance(path_payload, dict) + else [] + ) + has_bridgeable_unconnected = any( + isinstance(sample, dict) + and str(sample.get("access_carrier", "") or "").strip() + and ( + str(sample.get("nearest_network_carrier_name", "") or "").strip() + or str(sample.get("nearest_network_carrier_label", "") or "").strip() + ) + for sample in unconnected_samples + ) + if has_bridgeable_unconnected: + add("点击“按诊断建议生成桥接”尝试自动补未接入端子接入段到最近路径的 UserPath 桥") add("补端子附近 UserPath/线槽入口,或确认设备装配位置和端子接入最大距离") if ( "terminal_exit_direction_corrected" in issue_codes @@ -9048,6 +10187,10 @@ def _routing_connection_batch_issue_codes(report): "terminal_uuid_not_in_element", _safe_int(missing_endpoint_reason_counts.get("terminal_uuid_not_in_element", 0)) > 0, ), + ( + "duplicate_payload_terminal_instance_ids", + _safe_int(report.get("duplicate_payload_terminal_instance_id_count", 0)) > 0, + ), ( "missing_route_network", _safe_int(report.get("skipped_missing_route_network", 0)) > 0, @@ -9086,20 +10229,28 @@ def _routing_connection_batch_issue_codes(report): "long_terminal_access", bool(_long_network_entry_warning_samples(report, limit=1)), ), - ( - "route_candidate_obstacle_hits", - bool(_route_candidate_obstacle_warning_samples(report, limit=1)), - ), ( "route_candidate_boundary_violations", bool(_route_candidate_boundary_warning_samples(report, limit=1)), ), ( - "route_capacity_pressure", - bool(_route_capacity_pressure_samples(report, limit=1)), + "routed_wires_not_visible", + _safe_int((report.get("routed_wire_visibility") or {}).get("hidden", 0)) > 0, + ), + ( + "wire_styles_not_applied", + _safe_int((report.get("wire_style_application") or {}).get("missing_application", 0)) > 0, + ), + ( + "route_carriers_still_visible", + bool((report.get("route_carrier_visibility") or {}).get("expected_hidden")) + and _safe_int((report.get("route_carrier_visibility") or {}).get("visible_after_hide", 0)) > 0, ), ) - return [code for code, enabled in checks if enabled] + issue_codes = [code for code, enabled in checks if enabled] + # 候选避障命中与容量压力用于算法调试/质量观察;只要最终导线已生成且无真实碰撞, + # 它们不应让批量诊断失败,否则手动验收会把“候选评分过程”误读成最终布线问题。 + return issue_codes def _routed_route_issue_summary_from_report(report): @@ -9285,39 +10436,195 @@ def _find_route_bridge_sources_by_name_or_label(doc, name="", label=""): return refs -def _create_main_path_detour_bridges_from_report(doc, report, project_uuid=""): - detour_summary = report.get("main_path_detour_missing_summary", {}) if isinstance(report, dict) else {} - pair_counts = detour_summary.get("bridge_pair_counts", {}) if isinstance(detour_summary, dict) else {} - if not isinstance(pair_counts, dict): - pair_counts = {} +def _route_bridge_label(source, carrier, fallback): + return ( + getattr(source, "QetRouteSourceLabel", "") + or getattr(source, "Label", "") + or getattr(carrier, "QetRouteSourceLabel", "") + or getattr(carrier, "Label", "") + or getattr(carrier, "Name", "") + or fallback + ) - created = [] - missing_pairs = [] - duplicates = 0 - for pair_text, _count in sorted(pair_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0]))): - pair_text = str(pair_text or "").strip() - if " -> " not in pair_text: - continue - left_label, right_label = [part.strip() for part in pair_text.split(" -> ", 1)] - if not left_label or not right_label: - continue - left_matches = _find_route_bridge_sources_by_name_or_label(doc, name=left_label, label=left_label) - right_matches = _find_route_bridge_sources_by_name_or_label(doc, name=right_label, label=right_label) - if not left_matches or not right_matches: - missing_pairs.append(pair_text) - continue - new_bridges = RoutingNetwork.create_user_path_bridge_between_objects( - doc, - left_matches[0], - right_matches[0], - project_uuid=project_uuid, - ) - if new_bridges: - created.extend(new_bridges) - else: - duplicates += 1 - return { +def _is_auto_ignorable_unbound_structural_obstacle(obstacle): + if not isinstance(obstacle, dict): + return False + if ( + str(obstacle.get("element_uuid", "") or "").strip() + or str(obstacle.get("instance_id", "") or "").strip() + ): + return False + parent_refs = obstacle.get("parent_refs", {}) if isinstance(obstacle.get("parent_refs", {}), dict) else {} + own_text = " ".join( + str(part or "").lower() + for part in [ + obstacle.get("label", ""), + obstacle.get("name", ""), + ] + ) + if any(keyword in own_text for keyword in _DEVICE_COLLISION_KEYWORDS): + return False + text_parts = [ + obstacle.get("label", ""), + obstacle.get("name", ""), + ] + text_parts.extend(list(parent_refs.get("labels", []) or [])) + text_parts.extend(list(parent_refs.get("names", []) or [])) + text = " ".join(str(part or "").lower() for part in text_parts) + return any(keyword in text for keyword in _STRUCTURAL_COLLISION_KEYWORDS) + + +def _route_bridge_obstacles(doc, left_source, right_source, left_carrier, right_carrier): + options = { + "obstacle_clearance": float(DEFAULT_OPTIONS.get("obstacle_clearance", 5.0) or 0.0), + "ignore_endpoint_near_obstacles": False, + } + obstacles = collect_obstacles( + doc, + exclude=[left_source, right_source, left_carrier, right_carrier], + options=options, + ) + if not bool(DEFAULT_OPTIONS.get("auto_ignore_unbound_structural_obstacles", True)): + return obstacles + # 桥接路径在柜内生成,未绑定的柜体/安装框 AABB 往往包住整柜; + # 它们不能阻止线槽/UserPath 之间的局部连通,但真实设备仍保留为硬障碍。 + return [ + obstacle + for obstacle in obstacles + if not _is_auto_ignorable_unbound_structural_obstacle(obstacle) + ] + + +def _bridge_points_avoiding_obstacles(left_point, right_point, obstacles): + bboxes = [item.get("bbox") for item in list(obstacles or []) if isinstance(item.get("bbox"), dict)] + if not bboxes: + return [left_point, right_point] + # 自动桥接是布线路径网络的一部分,不能为了连通两条线槽而直接穿过设备。 + points = _orthogonal_points_avoiding_obstacles(left_point, right_point, bboxes) + if detect_collisions(points, obstacles): + return [] + return _simplify_collinear_points(points) + + +def _create_user_path_bridge_between_objects_avoiding_obstacles( + doc, + left_source, + right_source, + project_uuid="", + bridge_kind="MainPathDetourBridge", +): + best = RoutingNetwork.nearest_route_bridge_candidate_between_objects(doc, left_source, right_source) + if best is None: + return [] + left = best["left_carrier"] + right = best["right_carrier"] + left_point = best["left_point"] + right_point = best["right_point"] + if _distance(left_point, right_point) <= RoutingNetwork.DEFAULT_NODE_TOLERANCE: + return [] + try: + if RoutingNetwork._route_bridge_already_exists(doc, left_point, right_point): + return [] + except Exception: + pass + + obstacles = _route_bridge_obstacles(doc, left_source, right_source, left, right) + points = _bridge_points_avoiding_obstacles(left_point, right_point, obstacles) + if len(points) < 2: + return [] + + left_label = _route_bridge_label(left_source, left, "Path A") + right_label = _route_bridge_label(right_source, right, "Path B") + carrier = RoutingNetwork.create_route_carrier( + doc, + points, + label="QET User Bridge {0} -> {1}".format(left_label, right_label), + project_uuid=project_uuid, + kind=RoutingNetwork.ROUTE_CARRIER_KIND_USER_PATH, + capacity=min( + int(getattr(left, "QetRouteCarrierCapacity", 1) or 1), + int(getattr(right, "QetRouteCarrierCapacity", 1) or 1), + ), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeKind", + "QET Routing", + "QET route bridge kind", + str(bridge_kind or "MainPathDetourBridge"), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgePairLabel", + "QET Routing", + "Human readable source pair for this generated bridge", + "{0} -> {1}".format(left_label, right_label), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeLeftSourceName", + "QET Routing", + "Left/source object name for this generated bridge", + getattr(left_source, "Name", "") or getattr(left, "QetRouteSourceName", "") or getattr(left, "Name", ""), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeRightSourceName", + "QET Routing", + "Right/source object name for this generated bridge", + getattr(right_source, "Name", "") or getattr(right, "QetRouteSourceName", "") or getattr(right, "Name", ""), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeLeftSourceLabel", + "QET Routing", + "Left/source object label for this generated bridge", + left_label, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeRightSourceLabel", + "QET Routing", + "Right/source object label for this generated bridge", + right_label, + ) + return [carrier] + + +def _create_main_path_detour_bridges_from_report(doc, report, project_uuid=""): + detour_summary = report.get("main_path_detour_missing_summary", {}) if isinstance(report, dict) else {} + pair_counts = detour_summary.get("bridge_pair_counts", {}) if isinstance(detour_summary, dict) else {} + if not isinstance(pair_counts, dict): + pair_counts = {} + + created = [] + missing_pairs = [] + duplicates = 0 + for pair_text, _count in sorted(pair_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0]))): + pair_text = str(pair_text or "").strip() + if " -> " not in pair_text: + continue + left_label, right_label = [part.strip() for part in pair_text.split(" -> ", 1)] + if not left_label or not right_label: + continue + left_matches = _find_route_bridge_sources_by_name_or_label(doc, name=left_label, label=left_label) + right_matches = _find_route_bridge_sources_by_name_or_label(doc, name=right_label, label=right_label) + if not left_matches or not right_matches: + missing_pairs.append(pair_text) + continue + new_bridges = _create_user_path_bridge_between_objects_avoiding_obstacles( + doc, + left_matches[0], + right_matches[0], + project_uuid=project_uuid, + ) + if new_bridges: + created.extend(new_bridges) + else: + duplicates += 1 + + return { "enabled": True, "pairs": len(pair_counts), "created_count": len(created), @@ -9527,7 +10834,32 @@ def _create_terminal_access_fallback_bridges_from_report(doc, report, project_uu best_main = None best_distance = None + recommended_main_name = str(sample.get("nearest_main_path_name", "") or "").strip() + recommended_main_label = str(sample.get("nearest_main_path_label", "") or "").strip() + if recommended_main_name or recommended_main_label: + recommended_matches = _find_route_bridge_sources_by_name_or_label( + doc, + name=recommended_main_name, + label=recommended_main_label, + ) + for candidate in recommended_matches: + if candidate is target: + continue + if str(getattr(candidate, "QetRouteCarrierKind", "") or "").strip() not in main_path_kinds: + continue + bridge_candidate = RoutingNetwork.nearest_route_bridge_candidate_between_objects( + doc, + target, + candidate, + ) + if not isinstance(bridge_candidate, dict): + continue + best_main = candidate + best_distance = float(bridge_candidate.get("distance_mm", 0.0) or 0.0) + break for candidate in main_candidates: + if best_main is not None: + break if candidate is target: continue bridge_candidate = RoutingNetwork.nearest_route_bridge_candidate_between_objects( @@ -9546,7 +10878,7 @@ def _create_terminal_access_fallback_bridges_from_report(doc, report, project_uu if missing_ref not in missing_refs: missing_refs.append(missing_ref) continue - new_bridges = RoutingNetwork.create_user_path_bridge_between_objects( + new_bridges = _create_user_path_bridge_between_objects_avoiding_obstacles( doc, target, best_main, @@ -9572,6 +10904,216 @@ def _create_terminal_access_fallback_bridges_from_report(doc, report, project_uu } +def _main_path_target_bridge_kind_set(): + return {"WireDuct", "WireDuctOpenEnd", "UserPath", "WiringCutOut", "RoutingPath"} + + +def _create_main_path_target_bridges_from_report(doc, report, project_uuid=""): + """Bridge two main-path targets when a wire still detours through RoutingRange.""" + if not isinstance(report, dict): + return { + "enabled": True, + "pairs": 0, + "created_count": 0, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": [], + "wire_uuids": [], + "rerouted": False, + } + main_path_kinds = _main_path_target_bridge_kind_set() + seen_pairs = set() + wire_uuids = [] + created = [] + duplicates = 0 + missing_pairs = [] + + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + quality = _route_quality_payload(route.get("route_track", {})) + if not quality.get("fallback_carrier_kinds"): + continue + network = route.get("network", {}) if isinstance(route.get("network", {}), dict) else {} + start_kind = str(network.get("start_terminal_access_target_kind", "") or "").strip() + end_kind = str(network.get("end_terminal_access_target_kind", "") or "").strip() + if start_kind not in main_path_kinds or end_kind not in main_path_kinds: + continue + start_name = str(network.get("start_terminal_access_target_name", "") or "").strip() + end_name = str(network.get("end_terminal_access_target_name", "") or "").strip() + start_label = str(network.get("start_terminal_access_target_label", "") or "").strip() + end_label = str(network.get("end_terminal_access_target_label", "") or "").strip() + if not (start_name or start_label) or not (end_name or end_label): + continue + pair_key = tuple(sorted(((start_name or start_label), (end_name or end_label)))) + if len(set(pair_key)) < 2: + continue + wire_uuid = str(route.get("wire_uuid", "") or "").strip() + if pair_key in seen_pairs: + # 同一对线槽/UserPath 目标只需要补一条桥,但这对目标下的所有导线都应该重跑。 + if wire_uuid and wire_uuid not in wire_uuids: + wire_uuids.append(wire_uuid) + continue + seen_pairs.add(pair_key) + + start_matches = _find_route_bridge_sources_by_name_or_label( + doc, + name=start_name, + label=start_label, + ) + end_matches = _find_route_bridge_sources_by_name_or_label( + doc, + name=end_name, + label=end_label, + ) + if not start_matches or not end_matches: + missing_pairs.append("{0} -> {1}".format(start_label or start_name, end_label or end_name)) + continue + new_bridges = _create_user_path_bridge_between_objects_avoiding_obstacles( + doc, + start_matches[0], + end_matches[0], + project_uuid=project_uuid, + bridge_kind="MainPathTargetBridge", + ) + if new_bridges: + created.extend(new_bridges) + if wire_uuid and wire_uuid not in wire_uuids: + wire_uuids.append(wire_uuid) + else: + duplicates += 1 + + return { + "enabled": True, + "pairs": len(seen_pairs), + "created_count": len(created), + "duplicates": duplicates, + "missing_pairs": missing_pairs, + "created_pair_labels": [ + getattr(bridge, "QetRouteBridgePairLabel", "") + for bridge in created + ], + "wire_uuids": wire_uuids, + "rerouted": False, + } + + +def _terminal_access_target_ref(access_carrier): + if access_carrier is None: + return {} + kind = str(getattr(access_carrier, "QetTerminalAccessTargetKind", "") or "").strip() + name = str(getattr(access_carrier, "QetTerminalAccessTargetName", "") or "").strip() + label = str(getattr(access_carrier, "QetTerminalAccessTargetLabel", "") or "").strip() + if kind not in _main_path_target_bridge_kind_set(): + return {} + if not (name or label): + return {} + return { + "kind": kind, + "name": name, + "label": label, + "key": name or label, + } + + +def _create_main_path_target_bridges_from_payload(doc, payload, project_uuid=""): + """Pre-create bridges between the two main-path targets used by wire endpoints. + + 先补桥再布线,避免一批导线先退回 RoutingRange,随后又因为补桥重跑。 + """ + summary = { + "enabled": True, + "pairs": 0, + "created_count": 0, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": [], + "wire_uuids": [], + "rerouted": False, + "precreated_count": 0, + } + if doc is None or not isinstance(payload, dict): + return summary + payload = _load_context_payload_with_devices(payload) + payload = _load_document_payload_with_devices(doc, payload) + wires = list(payload.get("wires", []) or []) + if not wires: + return summary + _repair_duplicate_terminal_metadata_from_payload(doc, payload) + terminal_candidates = _collect_routable_terminals(doc) + seen_pairs = set() + created = [] + duplicates = 0 + missing_pairs = [] + wire_uuids = [] + + for item in wires: + if not isinstance(item, dict): + continue + start_terminal = _terminal_endpoint_match(terminal_candidates, item, "start").get("terminal") + end_terminal = _terminal_endpoint_match(terminal_candidates, item, "end").get("terminal") + if start_terminal is None or end_terminal is None: + continue + start_target = _terminal_access_target_ref( + RoutingNetwork.terminal_access_carrier_for_terminal(start_terminal) + ) + end_target = _terminal_access_target_ref( + RoutingNetwork.terminal_access_carrier_for_terminal(end_terminal) + ) + if not start_target or not end_target: + continue + pair_key = tuple(sorted((start_target["key"], end_target["key"]))) + if len(set(pair_key)) < 2: + continue + wire_uuid = _wire_item_uuid(item) + if wire_uuid and wire_uuid not in wire_uuids: + wire_uuids.append(wire_uuid) + if pair_key in seen_pairs: + continue + seen_pairs.add(pair_key) + start_matches = _find_route_bridge_sources_by_name_or_label( + doc, + name=start_target.get("name", ""), + label=start_target.get("label", ""), + ) + end_matches = _find_route_bridge_sources_by_name_or_label( + doc, + name=end_target.get("name", ""), + label=end_target.get("label", ""), + ) + if not start_matches or not end_matches: + missing_pairs.append( + "{0} -> {1}".format( + start_target.get("label") or start_target.get("name"), + end_target.get("label") or end_target.get("name"), + ) + ) + continue + new_bridges = _create_user_path_bridge_between_objects_avoiding_obstacles( + doc, + start_matches[0], + end_matches[0], + project_uuid=project_uuid, + bridge_kind="MainPathTargetBridge", + ) + if new_bridges: + created.extend(new_bridges) + else: + duplicates += 1 + + summary["pairs"] = len(seen_pairs) + summary["created_count"] = len(created) + summary["precreated_count"] = len(created) + summary["duplicates"] = duplicates + summary["missing_pairs"] = missing_pairs + summary["created_pair_labels"] = [ + getattr(bridge, "QetRouteBridgePairLabel", "") + for bridge in created + ] + summary["wire_uuids"] = wire_uuids + return summary + + def _wire_item_uuid(item): if not isinstance(item, dict): return "" @@ -9593,6 +11135,101 @@ def _payload_subset_for_wire_uuids(payload, wire_uuids): return subset if subset["wires"] else {} +def _append_unique_text(values, value): + result = [ + str(item or "").strip() + for item in list(values or []) + if str(item or "").strip() + ] + text = str(value or "").strip() + if text and text not in result: + result.append(text) + return result + + +def _same_main_path_target_retry_payload(payload, report): + """Build a retry payload for wires whose two accesses target main paths. + + 这类线在真实柜体里应优先沿线槽/UserPath/线槽开口等主路径走;如果第一次结果 + 仍退回 RoutingRange/AuxiliaryPath,就临时加“必经目标主路径、禁止兜底面”的约束重试。 + """ + summary = { + "enabled": True, + "wire_uuids": [], + "target_names": [], + "target_labels": [], + } + if not isinstance(payload, dict) or not isinstance(report, dict): + return {}, summary + main_path_kinds = _main_path_target_bridge_kind_set() + constraints_by_wire = {} + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + quality = _route_quality_payload(route.get("route_track", {})) + if not quality.get("fallback_carrier_kinds"): + continue + network = route.get("network", {}) if isinstance(route.get("network", {}), dict) else {} + start_kind = str(network.get("start_terminal_access_target_kind", "") or "").strip() + end_kind = str(network.get("end_terminal_access_target_kind", "") or "").strip() + if start_kind not in main_path_kinds or end_kind not in main_path_kinds: + continue + start_name = str(network.get("start_terminal_access_target_name", "") or "").strip() + end_name = str(network.get("end_terminal_access_target_name", "") or "").strip() + start_label = str(network.get("start_terminal_access_target_label", "") or "").strip() + end_label = str(network.get("end_terminal_access_target_label", "") or "").strip() + if not (start_name or start_label) or not (end_name or end_label): + continue + wire_uuid = str(route.get("wire_uuid", "") or "").strip() + if not wire_uuid: + continue + constraints_by_wire[wire_uuid] = { + "target_names": _append_unique_text([start_name], end_name), + "target_labels": _append_unique_text([start_label], end_label), + } + if wire_uuid not in summary["wire_uuids"]: + summary["wire_uuids"].append(wire_uuid) + for target_name in constraints_by_wire[wire_uuid]["target_names"]: + if target_name and target_name not in summary["target_names"]: + summary["target_names"].append(target_name) + for target_label in constraints_by_wire[wire_uuid]["target_labels"]: + if target_label and target_label not in summary["target_labels"]: + summary["target_labels"].append(target_label) + + if not constraints_by_wire: + return {}, summary + + retry_payload = dict(payload) + retry_wires = [] + for item in list(payload.get("wires", []) or []): + if not isinstance(item, dict): + continue + wire_uuid = _wire_item_uuid(item) + constraint = constraints_by_wire.get(wire_uuid) + if not constraint: + continue + retry_item = dict(item) + retry_item["forbidden_route_carrier_kinds"] = _append_unique_text( + _append_unique_text(retry_item.get("forbidden_route_carrier_kinds", []), "RoutingRange"), + "AuxiliaryPath", + ) + for target_name in list(constraint.get("target_names", []) or []): + retry_item["required_route_carrier_names"] = _append_unique_text( + retry_item.get("required_route_carrier_names", []), + target_name, + ) + for target_label in list(constraint.get("target_labels", []) or []): + retry_item["required_route_carrier_labels"] = _append_unique_text( + retry_item.get("required_route_carrier_labels", []), + target_label, + ) + retry_wires.append(retry_item) + if not retry_wires: + return {}, summary + retry_payload["wires"] = retry_wires + return retry_payload, summary + + def _recompute_route_report_after_route_replacement(doc, report): routes = [route for route in list((report or {}).get("routes", []) or []) if isinstance(route, dict)] route_status_counts = {} @@ -9752,6 +11389,7 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): "context_devices_json_path", "runtime_version", "hidden_route_carriers", + "duplicate_payload_terminal_instance_id_count", "routing_method", "routing_path_network_updated", ) @@ -9769,12 +11407,23 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): payload["routing_path_network_diagnostic"] = report.get("routing_path_network_diagnostic") if isinstance(report.get("auto_diagnostic_bridges"), dict): payload["auto_diagnostic_bridges"] = dict(report.get("auto_diagnostic_bridges") or {}) + if isinstance(report.get("auto_main_path_target_bridges"), dict): + payload["auto_main_path_target_bridges"] = dict(report.get("auto_main_path_target_bridges") or {}) + if isinstance(report.get("same_main_path_target_retry"), dict): + payload["same_main_path_target_retry"] = dict(report.get("same_main_path_target_retry") or {}) if isinstance(report.get("auto_main_path_detour_bridges"), dict): payload["auto_main_path_detour_bridges"] = dict(report.get("auto_main_path_detour_bridges") or {}) if isinstance(report.get("auto_terminal_access_fallback_bridges"), dict): payload["auto_terminal_access_fallback_bridges"] = dict( report.get("auto_terminal_access_fallback_bridges") or {} ) + for key in ( + "routed_wire_visibility", + "wire_style_application", + "route_carrier_visibility", + ): + if isinstance(report.get(key), dict): + payload[key] = dict(report.get(key) or {}) if isinstance(report.get("route_status_counts"), dict): payload["route_status_counts"] = dict(report.get("route_status_counts") or {}) carrier_kind_counts = _report_route_network_carrier_kind_counts(report) @@ -9797,6 +11446,7 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): payload["missing_terminal_summary"] = missing_terminal_summary for key in ( "auto_terminal_binding_warnings", + "duplicate_payload_terminal_instance_id_samples", "missing_endpoint_samples", "missing_route_network_samples", "collision_samples", @@ -10000,15 +11650,37 @@ def _direct_task_routing_path_network_diagnostic(doc, opts): } +def _diagnostic_bridge_summary_from_report(result, enabled=True): + if not isinstance(result, dict): + result = {} + created = list(result.get("created", []) or []) + return { + "enabled": bool(enabled), + "suggestions": int(result.get("suggestions", 0) or 0), + "created_count": len(created), + "duplicates": int(result.get("duplicates", 0) or 0), + "stale_suggestions": int(result.get("stale_suggestions", 0) or 0), + "unconnected_terminal_access_bridge_targets": int( + result.get("unconnected_terminal_access_bridge_targets", 0) or 0 + ), + "unconnected_terminal_access_user_path_bridges": int( + result.get("unconnected_terminal_access_user_path_bridges", 0) or 0 + ), + "unconnected_terminal_access_bridge_duplicates": int( + result.get("unconnected_terminal_access_bridge_duplicates", 0) or 0 + ), + "unconnected_terminal_access_bridge_pair_labels": list( + result.get("unconnected_terminal_access_bridge_pair_labels", []) or [] + ), + } + + def _direct_task_auto_diagnostic_bridge_report(doc, opts): diagnostic = _direct_task_routing_path_network_diagnostic(doc, opts) - bridge_report = { - "enabled": bool(opts.get("auto_create_diagnostic_bridges", True)), - "suggestions": 0, - "created_count": 0, - "duplicates": 0, - "stale_suggestions": 0, - } + bridge_report = _diagnostic_bridge_summary_from_report( + {}, + enabled=bool(opts.get("auto_create_diagnostic_bridges", True)), + ) if not bridge_report["enabled"]: return diagnostic, bridge_report try: @@ -10018,25 +11690,13 @@ def _direct_task_auto_diagnostic_bridge_report(doc, opts): project_uuid=_project_uuid(doc), ) created = list(result.get("created", []) or []) if isinstance(result, dict) else [] - bridge_report = { - "enabled": True, - "suggestions": int(result.get("suggestions", 0) or 0), - "created_count": len(created), - "duplicates": int(result.get("duplicates", 0) or 0), - "stale_suggestions": int(result.get("stale_suggestions", 0) or 0), - } + bridge_report = _diagnostic_bridge_summary_from_report(result, enabled=True) if created: # 任务直连入口没有“更新路径网络”前置步骤;桥接创建后补一次诊断,让报告反映桥接后的网络状态。 diagnostic = _direct_task_routing_path_network_diagnostic(doc, opts) except Exception as exc: - bridge_report = { - "enabled": True, - "suggestions": 0, - "created_count": 0, - "duplicates": 0, - "stale_suggestions": 0, - "error": str(exc), - } + bridge_report = _diagnostic_bridge_summary_from_report({}, enabled=True) + bridge_report["error"] = str(exc) return diagnostic, bridge_report @@ -10181,6 +11841,12 @@ def _compact_routing_path_network_diagnostic(diagnostic): } for item in outside_terminals[:5] ] + unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or []) + if unconnected: + payload["unconnected_terminals"] = [ + _compact_unconnected_terminal_sample(item) + for item in unconnected[:8] + ] long_accesses = _dict_items(diagnostic.get("long_terminal_accesses", []) or []) if long_accesses: payload["long_terminal_accesses"] = [ @@ -10198,6 +11864,19 @@ def _compact_routing_path_network_diagnostic(diagnostic): "terminal_access_length_mm": item.get("terminal_access_length_mm", 0.0), "terminal_access_warning_distance_mm": item.get("terminal_access_warning_distance_mm", 0.0), "terminal_access_max_distance_mm": item.get("terminal_access_max_distance_mm", 0.0), + "target_kind": item.get("target_kind", ""), + "target_name": item.get("target_name", ""), + "target_label": item.get("target_label", ""), + "target_rule": item.get("target_rule", ""), + "target_distance_mm": item.get("target_distance_mm", 0.0), + "nearest_main_path_kind": item.get("nearest_main_path_kind", ""), + "nearest_main_path_name": item.get("nearest_main_path_name", ""), + "nearest_main_path_label": item.get("nearest_main_path_label", ""), + "nearest_main_path_distance_mm": item.get("nearest_main_path_distance_mm", 0.0), + "nearest_main_path_over_max_distance": bool( + item.get("nearest_main_path_over_max_distance", False) + ), + "endpoint_device_avoided": bool(item.get("endpoint_device_avoided", False)), "terminal_access_dominant_axis": item.get("terminal_access_dominant_axis", ""), "terminal_access_axis_lengths_mm": item.get("terminal_access_axis_lengths_mm", {}), "terminal_access_points": list(item.get("terminal_access_points", []) or [])[:6], @@ -10222,6 +11901,12 @@ def _compact_routing_path_network_diagnostic(diagnostic): _compact_terminal_metadata_issue_sample(item) for item in invalid_exit_directions[:8] ] + invalid_local_routes = _dict_items(diagnostic.get("invalid_terminal_local_routes", []) or []) + if invalid_local_routes: + payload["invalid_terminal_local_routes"] = [ + _compact_terminal_metadata_issue_sample(item) + for item in invalid_local_routes[:8] + ] wire_duct_components = _dict_items(diagnostic.get("wire_ducts_without_terminal_access", []) or []) if wire_duct_components: payload["wire_ducts_without_terminal_access"] = [ @@ -10254,6 +11939,33 @@ def _compact_routing_path_network_diagnostic(diagnostic): return payload +def _compact_unconnected_terminal_sample(item): + return { + "name": item.get("name", ""), + "label": item.get("label", ""), + "terminal_uuid": item.get("terminal_uuid", ""), + "instance_id": item.get("instance_id", ""), + "terminal_origin": item.get("terminal_origin", {}), + "parent_device_name": item.get("parent_device_name", ""), + "parent_device_label": item.get("parent_device_label", ""), + "parent_device_instance_id": item.get("parent_device_instance_id", ""), + "parent_device_element_uuid": item.get("parent_device_element_uuid", ""), + "access_carrier": item.get("access_carrier", ""), + "nearest_network_distance_mm": item.get("nearest_network_distance_mm"), + "nearest_network_point": item.get("nearest_network_point"), + "nearest_network_carrier_kind": item.get("nearest_network_carrier_kind", ""), + "nearest_network_carrier_name": item.get("nearest_network_carrier_name", ""), + "nearest_network_carrier_label": item.get("nearest_network_carrier_label", ""), + "terminal_access_max_distance_mm": item.get("terminal_access_max_distance_mm", 0.0), + "terminal_exit_length_mm": item.get("terminal_exit_length_mm", 0.0), + "terminal_exit_point": item.get("terminal_exit_point", {}), + "terminal_access_dominant_axis": item.get("terminal_access_dominant_axis", ""), + "terminal_access_axis_lengths_mm": item.get("terminal_access_axis_lengths_mm", {}), + "terminal_access_points": list(item.get("terminal_access_points", []) or [])[:6], + "code": item.get("code", ""), + } + + def _compact_terminal_access_quality_sample(item): return { "access_carrier_name": item.get("access_carrier_name", ""), @@ -10271,11 +11983,21 @@ def _compact_terminal_access_quality_sample(item): "target_label": item.get("target_label", ""), "target_rule": item.get("target_rule", ""), "target_distance_mm": item.get("target_distance_mm", 0.0), + "terminal_access_max_distance_mm": item.get("terminal_access_max_distance_mm", 0.0), + "nearest_main_path_kind": item.get("nearest_main_path_kind", ""), + "nearest_main_path_name": item.get("nearest_main_path_name", ""), + "nearest_main_path_label": item.get("nearest_main_path_label", ""), + "nearest_main_path_distance_mm": item.get("nearest_main_path_distance_mm", 0.0), + "nearest_main_path_over_max_distance": bool(item.get("nearest_main_path_over_max_distance", False)), + "endpoint_device_avoided": bool(item.get("endpoint_device_avoided", False)), + "endpoint_device_bbox": item.get("endpoint_device_bbox", {}), + "access_length_mm": item.get("access_length_mm", 0.0), + "access_points": list(item.get("access_points", []) or [])[:6], } def _compact_terminal_metadata_issue_sample(item): - return { + payload = { "name": item.get("name", ""), "label": item.get("label", ""), "terminal_uuid": item.get("terminal_uuid", ""), @@ -10289,6 +12011,10 @@ def _compact_terminal_metadata_issue_sample(item): "message": item.get("message", ""), "raw_sample": item.get("raw_sample", ""), } + for key in ("local_route_end_point", "endpoint_device_bbox", "valid_point_count"): + if key in item: + payload[key] = item.get(key) + return payload def _compact_terminal_exit_diagnostic_sample(item): @@ -10306,6 +12032,10 @@ def _compact_terminal_exit_diagnostic_sample(item): "exit_direction": item.get("exit_direction", {}), "original_exit_direction": item.get("original_exit_direction", {}), "exit_direction_corrected": bool(item.get("exit_direction_corrected", False)), + "origin": item.get("origin", {}), + "exit_point": item.get("exit_point", {}), + "local_route_used": bool(item.get("local_route_used", False)), + "local_route_point_count": _safe_int(item.get("local_route_point_count", 0)), "requested_exit_length_mm": item.get("requested_exit_length_mm", 0.0), "actual_exit_length_mm": item.get("actual_exit_length_mm", 0.0), "max_exit_length_mm": item.get("max_exit_length_mm", 0.0), @@ -10372,6 +12102,20 @@ def _diagnostic_terminal_text(item): return terminal_uuid or display or "未知端子" +def _diagnostic_nearest_network_carrier_text(item): + if not isinstance(item, dict): + return "" + label = str(item.get("nearest_network_carrier_label", "") or "").strip() + name = str(item.get("nearest_network_carrier_name", "") or "").strip() + kind = str(item.get("nearest_network_carrier_kind", "") or "").strip() + text = label or name + if not text: + return "" + if kind: + return "{0}({1})".format(text, kind) + return text + + def _dict_items(value): if not isinstance(value, list): return [] @@ -10411,9 +12155,12 @@ def format_routing_path_network_report(diagnostic): unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or []) if unconnected: sample = unconnected[0] - message += "\n端子未接入:{0},距离最近网络 {1},当前端子接入最大距离 {2}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format( + nearest_carrier = _diagnostic_nearest_network_carrier_text(sample) + nearest_carrier_clause = ",最近路径 {0}".format(nearest_carrier) if nearest_carrier else "" + message += "\n端子未接入:{0},距离最近网络 {1}{2},当前端子接入最大距离 {3}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format( _diagnostic_terminal_text(sample), _format_distance_mm(sample.get("nearest_network_distance_mm")), + nearest_carrier_clause, _format_distance_mm(sample.get("terminal_access_max_distance_mm")), ) @@ -10476,9 +12223,18 @@ def format_routing_path_network_report(diagnostic): long_accesses = _dict_items(diagnostic.get("long_terminal_accesses", []) or []) if long_accesses: sample = long_accesses[0] - message += "\n端子接入过长:{0},接入段 {1},建议补设备局部路径、移动设备或补一段 UserPath/线槽靠近端子。".format( + target_label = str(sample.get("target_label", "") or sample.get("target_name", "") or "").strip() + target_kind = str(sample.get("target_kind", "") or "").strip() + target_clause = "" + if target_label or target_kind: + target_text = target_label or "未知目标" + if target_kind: + target_text = "{0}({1})".format(target_text, target_kind) + target_clause = ",目标 {0}".format(target_text) + message += "\n端子接入过长:{0},接入段 {1}{2},建议补设备局部路径、移动设备或补一段 UserPath/线槽靠近端子。".format( _diagnostic_terminal_text(sample), _format_distance_mm(sample.get("terminal_access_length_mm")), + target_clause, ) invalid_exit_directions = _dict_items(diagnostic.get("invalid_terminal_exit_directions", []) or []) @@ -10492,9 +12248,31 @@ def format_routing_path_network_report(diagnostic): invalid_local_routes = _dict_items(diagnostic.get("invalid_terminal_local_routes", []) or []) if invalid_local_routes: sample = invalid_local_routes[0] - message += "\n端子局部路径无效:{0},字段 {1}。请检查模板端子局部路径或 QetTerminalLocalRoutePointsJson。".format( + reason = str(sample.get("reason", "") or "").strip() + reason_clause = ",原因 {0}".format(reason) if reason else "" + message += "\n端子局部路径无效:{0},字段 {1}{2}。请检查模板端子局部路径或 QetTerminalLocalRoutePointsJson。".format( _diagnostic_terminal_text(sample), sample.get("property_name", "未知字段"), + reason_clause, + ) + + corrected_exits = _dict_items(diagnostic.get("corrected_terminal_exits", []) or []) + if corrected_exits: + sample = corrected_exits[0] + message += "\n端子默认出线方向已校正:{0},原方向 {1},采用方向 {2}。建议复查设备模板端子 LCS 或补明确局部出线路径。".format( + _diagnostic_terminal_text(sample), + _format_point_text(sample.get("original_exit_direction")), + _format_point_text(sample.get("exit_direction")), + ) + + capped_exits = _dict_items(diagnostic.get("capped_terminal_exits", []) or []) + if capped_exits: + sample = capped_exits[0] + message += "\n端子出线长度截断:{0},实际 {1} / 上限 {2},设备出线需求 {3}。建议检查父设备包围盒、端子方向或局部出线路径。".format( + _diagnostic_terminal_text(sample), + _format_distance_mm(sample.get("actual_exit_length_mm")), + _format_distance_mm(sample.get("max_exit_length_mm")), + _format_distance_mm(sample.get("device_exit_required_length_mm")), ) routing_range_only = diagnostic.get("routing_range_only_network", {}) @@ -10510,7 +12288,7 @@ def format_routing_path_network_report(diagnostic): carrier_text = "、".join([str(item) for item in carriers[:3]]) if carriers else "未知 carrier" message += "\n存在孤立路径网络:{0}。请用线槽/辅助路径把孤立网络接入主网络。".format(carrier_text) - if not (empty_network or unconnected or possible_breaks or wire_duct_components or invalid_carriers or outside_carriers or outside_terminals or long_accesses or invalid_exit_directions or invalid_local_routes or routing_range_only or isolated): + if not (empty_network or unconnected or possible_breaks or wire_duct_components or invalid_carriers or outside_carriers or outside_terminals or long_accesses or invalid_exit_directions or invalid_local_routes or corrected_exits or capped_exits or routing_range_only or isolated): first_issue = issues[0] message += "\n首个问题:{0} ({1})。".format( first_issue.get("code", "unknown"), @@ -10529,6 +12307,33 @@ def update_eplan_routing_path_network(doc, project_uuid="", options=None, select ) +def _refresh_terminal_access_after_route_network_change(doc, project_uuid="", options=None): + """Refresh only TerminalAccess carriers after auto-created bridge paths. + + 自动补 UserPath/桥接路径后,端子接入段可能还指向旧线槽。这里不重建 + 线槽、布线面等整套网络,只重算 TerminalAccess,让端子重新选择最近且 + 更合适的主路径,兼顾质量和耗时。 + """ + if doc is None: + return {"refreshed": False, "terminal_access_carriers": 0} + opts = options if isinstance(options, dict) else _merged_options(options) + _invalidate_route_network_cache(opts) + try: + carriers = RoutingNetwork.create_terminal_access_carriers_from_document( + doc, + project_uuid=project_uuid, + terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0), + terminal_exit_max_length=float(opts.get("terminal_exit_max_length", 80.0) or 0.0), + max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), + ) + finally: + _invalidate_route_network_cache(opts) + return { + "refreshed": True, + "terminal_access_carriers": len(carriers), + } + + def route_eplan_connections( doc, payload=None, @@ -10541,6 +12346,27 @@ def route_eplan_connections( if doc is None: raise AutoRoutingError("No FreeCAD document is available.") opts = _merged_options(options) + opts.setdefault("__route_network_cache", {}) + terminal_access_refreshes = [] + + def refresh_terminal_access(reason): + refresh = {"reason": reason, "refreshed": False, "terminal_access_carriers": 0} + try: + refresh.update( + _refresh_terminal_access_after_route_network_change( + doc, + project_uuid=(project_uuid or _project_uuid(doc)), + options=opts, + ) + ) + if isinstance(prepared_network, dict): + prepared_network["terminal_access_carriers"] = int( + refresh.get("terminal_access_carriers", 0) or 0 + ) + except Exception as exc: + refresh["error"] = str(exc) + terminal_access_refreshes.append(refresh) + return refresh prepared_network = None if update_network: @@ -10550,6 +12376,46 @@ def route_eplan_connections( options=opts, selection_ex=selection_ex, ) + + target_payload = payload + if target_payload is None: + candidate_payload = getattr(App, "_qet_exchange_payload", None) + if _payload_matches_document_project(doc, candidate_payload): + target_payload = candidate_payload + + effective_route_payload = target_payload if isinstance(target_payload, dict) and target_payload.get("wires") else None + precreated_main_path_target_bridges = { + "enabled": bool(opts.get("auto_create_main_path_target_bridges", True)), + "pairs": 0, + "created_count": 0, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": [], + "wire_uuids": [], + "rerouted": False, + "precreated_count": 0, + } + if ( + bool(opts.get("auto_create_main_path_target_bridges", True)) + and isinstance(effective_route_payload, dict) + and effective_route_payload.get("wires") + ): + try: + precreated_main_path_target_bridges = _create_main_path_target_bridges_from_payload( + doc, + effective_route_payload, + project_uuid=(project_uuid or _project_uuid(doc)), + ) + if int(precreated_main_path_target_bridges.get("created_count", 0) or 0) > 0: + refresh_terminal_access("precreated_main_path_target_bridges") + except Exception as exc: + precreated_main_path_target_bridges["error"] = str(exc) + + diagnostic_route_network = None + try: + diagnostic_route_network, _diagnostic_route_network_reused = _cached_base_route_network(doc, opts) + except Exception: + diagnostic_route_network = None routing_path_network_diagnostic = {} auto_diagnostic_bridges = { "enabled": bool(opts.get("auto_create_diagnostic_bridges", True)), @@ -10557,6 +12423,10 @@ def route_eplan_connections( "created_count": 0, "duplicates": 0, "stale_suggestions": 0, + "unconnected_terminal_access_bridge_targets": 0, + "unconnected_terminal_access_user_path_bridges": 0, + "unconnected_terminal_access_bridge_duplicates": 0, + "unconnected_terminal_access_bridge_pair_labels": [], } try: routing_path_network_diagnostic = _compact_routing_path_network_diagnostic( @@ -10567,6 +12437,7 @@ def route_eplan_connections( terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + route_network=diagnostic_route_network, ) ) except Exception as exc: @@ -10593,14 +12464,9 @@ def route_eplan_connections( project_uuid=(project_uuid or _project_uuid(doc)), ) created = list(bridge_report.get("created", []) or []) if isinstance(bridge_report, dict) else [] - auto_diagnostic_bridges = { - "enabled": True, - "suggestions": int(bridge_report.get("suggestions", 0) or 0), - "created_count": len(created), - "duplicates": int(bridge_report.get("duplicates", 0) or 0), - "stale_suggestions": int(bridge_report.get("stale_suggestions", 0) or 0), - } + auto_diagnostic_bridges = _diagnostic_bridge_summary_from_report(bridge_report, enabled=True) if created: + _invalidate_route_network_cache(opts) if update_network: prepared_network = update_eplan_routing_path_network( doc, @@ -10608,6 +12474,12 @@ def route_eplan_connections( options=opts, selection_ex=selection_ex, ) + else: + refresh_terminal_access("auto_diagnostic_bridges") + try: + diagnostic_route_network, _diagnostic_route_network_reused = _cached_base_route_network(doc, opts) + except Exception: + diagnostic_route_network = None routing_path_network_diagnostic = _compact_routing_path_network_diagnostic( RoutingNetwork.diagnose_routing_path_network( doc, @@ -10616,23 +12488,12 @@ def route_eplan_connections( terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0), terminal_access_warning_distance=float(opts.get("terminal_access_warning_distance", 0.0) or 0.0), adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + route_network=diagnostic_route_network, ) ) except Exception as exc: - auto_diagnostic_bridges = { - "enabled": True, - "suggestions": 0, - "created_count": 0, - "duplicates": 0, - "stale_suggestions": 0, - "error": str(exc), - } - - target_payload = payload - if target_payload is None: - candidate_payload = getattr(App, "_qet_exchange_payload", None) - if _payload_matches_document_project(doc, candidate_payload): - target_payload = candidate_payload + auto_diagnostic_bridges = _diagnostic_bridge_summary_from_report({}, enabled=True) + auto_diagnostic_bridges["error"] = str(exc) if isinstance(target_payload, dict) and target_payload.get("wires"): report = route_eplan_connections_from_payload( @@ -10644,12 +12505,153 @@ def route_eplan_connections( else: task_route_options = dict(opts) task_route_options["__skip_task_auto_diagnostic_bridges"] = True + effective_route_payload = _wire_tasks_payload(doc) + if bool(opts.get("auto_create_main_path_target_bridges", True)): + try: + precreated_main_path_target_bridges = _create_main_path_target_bridges_from_payload( + doc, + effective_route_payload, + project_uuid=(project_uuid or _project_uuid(doc)), + ) + if int(precreated_main_path_target_bridges.get("created_count", 0) or 0) > 0: + refresh_terminal_access("precreated_task_main_path_target_bridges") + except Exception as exc: + precreated_main_path_target_bridges["error"] = str(exc) report = route_eplan_connection_tasks( doc, options=task_route_options, prepared_layout=prepared_network, ) + auto_main_path_target_bridges = dict(precreated_main_path_target_bridges) + if bool(opts.get("auto_create_main_path_target_bridges", True)): + try: + post_main_path_target_bridges = _create_main_path_target_bridges_from_report( + doc, + report, + project_uuid=(project_uuid or _project_uuid(doc)), + ) + auto_main_path_target_bridges["pairs"] = int(auto_main_path_target_bridges.get("pairs", 0) or 0) + int( + post_main_path_target_bridges.get("pairs", 0) or 0 + ) + auto_main_path_target_bridges["created_count"] = int( + auto_main_path_target_bridges.get("created_count", 0) or 0 + ) + int(post_main_path_target_bridges.get("created_count", 0) or 0) + auto_main_path_target_bridges["postcreated_count"] = int( + post_main_path_target_bridges.get("created_count", 0) or 0 + ) + auto_main_path_target_bridges["duplicates"] = int( + auto_main_path_target_bridges.get("duplicates", 0) or 0 + ) + int(post_main_path_target_bridges.get("duplicates", 0) or 0) + auto_main_path_target_bridges["missing_pairs"] = list(auto_main_path_target_bridges.get("missing_pairs", []) or []) + list( + post_main_path_target_bridges.get("missing_pairs", []) or [] + ) + auto_main_path_target_bridges["created_pair_labels"] = list(auto_main_path_target_bridges.get("created_pair_labels", []) or []) + list( + post_main_path_target_bridges.get("created_pair_labels", []) or [] + ) + auto_main_path_target_bridges["wire_uuids"] = _append_unique_text( + auto_main_path_target_bridges.get("wire_uuids", []), + "", + ) + for wire_uuid in list(post_main_path_target_bridges.get("wire_uuids", []) or []): + auto_main_path_target_bridges["wire_uuids"] = _append_unique_text( + auto_main_path_target_bridges.get("wire_uuids", []), + wire_uuid, + ) + if int(post_main_path_target_bridges.get("created_count", 0) or 0) > 0: + retry_wire_uuids = list(post_main_path_target_bridges.get("wire_uuids", []) or []) + refresh_terminal_access("postcreated_main_path_target_bridges") + retry_payload = _payload_subset_for_wire_uuids(effective_route_payload, retry_wire_uuids) + if isinstance(retry_payload, dict) and retry_payload.get("wires"): + retry_report = route_eplan_connections_from_payload( + doc, + retry_payload, + options=opts, + prepared_layout=prepared_network, + ) + report = _merge_retry_routes_into_report( + doc, + report, + retry_report, + retry_prefix="main_path_target", + ) + auto_main_path_target_bridges["retry_wires"] = len(retry_payload.get("wires", []) or []) + auto_main_path_target_bridges["retry_replaced_routes"] = int( + report.get("main_path_target_retry_replaced_routes", 0) or 0 + ) + auto_main_path_target_bridges["rerouted"] = True + else: + auto_main_path_target_bridges["retry_wires"] = 0 + auto_main_path_target_bridges["retry_replaced_routes"] = 0 + auto_main_path_target_bridges["rerouted"] = False + else: + auto_main_path_target_bridges.setdefault("retry_wires", 0) + auto_main_path_target_bridges.setdefault("retry_replaced_routes", 0) + auto_main_path_target_bridges.setdefault("rerouted", False) + except Exception as exc: + auto_main_path_target_bridges = { + "enabled": True, + "pairs": 0, + "created_count": 0, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": [], + "wire_uuids": [], + "rerouted": False, + "precreated_count": int(precreated_main_path_target_bridges.get("precreated_count", 0) or 0), + "error": str(exc), + } + + same_main_path_target_retry = { + "enabled": True, + "wire_uuids": [], + "target_names": [], + "target_labels": [], + "retry_wires": 0, + "retry_replaced_routes": 0, + "rerouted": False, + } + try: + retry_payload, same_main_path_target_retry = _same_main_path_target_retry_payload( + effective_route_payload, + report, + ) + if isinstance(retry_payload, dict) and retry_payload.get("wires"): + retry_report = route_eplan_connections_from_payload( + doc, + retry_payload, + options=opts, + prepared_layout=prepared_network, + ) + report = _merge_retry_routes_into_report( + doc, + report, + retry_report, + retry_prefix="same_main_path_target", + ) + same_main_path_target_retry["retry_wires"] = len(retry_payload.get("wires", []) or []) + same_main_path_target_retry["retry_replaced_routes"] = int( + report.get("same_main_path_target_retry_replaced_routes", 0) or 0 + ) + same_main_path_target_retry["rerouted"] = bool( + same_main_path_target_retry["retry_replaced_routes"] > 0 + ) + else: + same_main_path_target_retry.setdefault("retry_wires", 0) + same_main_path_target_retry.setdefault("retry_replaced_routes", 0) + same_main_path_target_retry.setdefault("rerouted", False) + except Exception as exc: + same_main_path_target_retry = { + "enabled": True, + "wire_uuids": [], + "target_names": [], + "target_labels": [], + "retry_wires": 0, + "retry_replaced_routes": 0, + "rerouted": False, + "error": str(exc), + } + auto_main_path_detour_bridges = { "enabled": bool(opts.get("auto_create_main_path_detour_bridges", True)), "pairs": 0, @@ -10668,14 +12670,8 @@ def route_eplan_connections( ) if int(auto_main_path_detour_bridges.get("created_count", 0) or 0) > 0: retry_wire_uuids = _main_path_detour_wire_uuids_from_report(report) - if update_network: - prepared_network = update_eplan_routing_path_network( - doc, - project_uuid=project_uuid, - options=opts, - selection_ex=selection_ex, - ) - retry_payload = _payload_subset_for_wire_uuids(target_payload, retry_wire_uuids) + refresh_terminal_access("auto_main_path_detour_bridges") + retry_payload = _payload_subset_for_wire_uuids(effective_route_payload, retry_wire_uuids) if isinstance(retry_payload, dict) and retry_payload.get("wires"): retry_report = route_eplan_connections_from_payload( doc, @@ -10723,14 +12719,8 @@ def route_eplan_connections( ) if int(auto_terminal_access_fallback_bridges.get("created_count", 0) or 0) > 0: retry_wire_uuids = _terminal_access_fallback_wire_uuids_from_report(report) - if update_network: - prepared_network = update_eplan_routing_path_network( - doc, - project_uuid=project_uuid, - options=opts, - selection_ex=selection_ex, - ) - retry_payload = _payload_subset_for_wire_uuids(target_payload, retry_wire_uuids) + refresh_terminal_access("auto_terminal_access_fallback_bridges") + retry_payload = _payload_subset_for_wire_uuids(effective_route_payload, retry_wire_uuids) if isinstance(retry_payload, dict) and retry_payload.get("wires"): retry_report = route_eplan_connections_from_payload( doc, @@ -10771,14 +12761,25 @@ def route_eplan_connections( report["routing_path_network_updated"] = bool(update_network) report["routing_path_network_diagnostic"] = routing_path_network_diagnostic report["auto_diagnostic_bridges"] = auto_diagnostic_bridges + report["auto_main_path_target_bridges"] = auto_main_path_target_bridges + report["same_main_path_target_retry"] = same_main_path_target_retry report["auto_main_path_detour_bridges"] = auto_main_path_detour_bridges report["auto_terminal_access_fallback_bridges"] = auto_terminal_access_fallback_bridges + report["terminal_access_refreshes"] = terminal_access_refreshes if isinstance(prepared_network, dict): report["routing_path_network"] = prepared_network if opts.get("hide_route_carriers_after_route", True): report["hidden_route_carriers"] = RoutingNetwork.set_route_carriers_visibility(doc, False) else: report["hidden_route_carriers"] = 0 + report["visible_routed_wires"] = _ensure_routed_wires_visible_and_styled(doc) + report["routed_wire_visibility"] = _routed_wire_visibility_summary(doc) + report["wire_style_application"] = _wire_style_application_summary(doc) + report["route_carrier_visibility"] = _route_carrier_visibility_summary( + doc, + expected_hidden=bool(opts.get("hide_route_carriers_after_route", True)), + ) + _refresh_routing_view(doc) _write_routing_connection_batch_diagnostic(doc, report) return report diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 8176615..eb5beef 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -419,11 +419,24 @@ class AutoRoutingController: def check_routing_readiness(self): doc = _active_document() payload = getattr(App, "_qet_exchange_payload", None) + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + prepared_layout = None + try: + # 手动验收时用户常直接点“检查布线准备度”。这里先按当前文档刷新一次 + # 路径网络,让预检反映真实可布线状态,而不是停留在“尚未点生成路径网络”。 + prepared_layout = AutoRouting.generate_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=self.routing_options(), + ) + except Exception as exc: + prepared_layout = {"error": str(exc)} self.last_report = AutoRouting.preflight_eplan_connections( doc, payload=payload if isinstance(payload, dict) else None, options=self.routing_options(), ) + self.last_report["prepared_layout"] = prepared_layout AutoRouting.write_routing_preflight_diagnostic(doc, self.last_report) return self.last_report @@ -433,6 +446,11 @@ class AutoRoutingController: AutoRouting.write_routing_diagnostic_summary(doc, self.last_report) return self.last_report + def apply_phase1_acceptance_view(self): + doc = _active_document() + self.last_report = AutoRouting.apply_phase1_acceptance_view(doc) + return self.last_report + def select_top_collision_obstacles(self): doc = _active_document() summary = AutoRouting.collect_routing_diagnostic_summary(doc) @@ -1891,6 +1909,8 @@ class AutoRoutingController: selected = [] selected_terminals = [] selected_devices = [] + selected_access_carriers = [] + selected_nearest_paths = [] selected_names = set() missing_refs = [] max_distance = 0.0 @@ -1967,16 +1987,45 @@ class AutoRoutingController: else: add_selection(parent_device, selected_devices) + access_carrier_name = str(sample.get("access_carrier", "") or "").strip() + if access_carrier_name: + access_carrier = doc.getObject(access_carrier_name) + if access_carrier is None: + remember_missing(access_carrier_name) + else: + add_selection(access_carrier, selected_access_carriers) + + nearest_path_name = str(sample.get("nearest_network_carrier_name", "") or "").strip() + nearest_path_label = str(sample.get("nearest_network_carrier_label", "") or "").strip() + if nearest_path_name or nearest_path_label: + nearest_path = self._find_object_by_name_or_label( + doc, + nearest_path_name, + nearest_path_label, + ) + if nearest_path is None: + remember_missing(nearest_path_name or nearest_path_label) + else: + add_selection(nearest_path, selected_nearest_paths) + self.last_report = { "selected_unconnected_terminal_access_objects": len(selected), "selected_unconnected_terminal_access_terminals": len(selected_terminals), "selected_unconnected_terminal_access_devices": len(selected_devices), + "selected_unconnected_terminal_access_carriers": len(selected_access_carriers), + "selected_unconnected_terminal_access_nearest_paths": len(selected_nearest_paths), "selected_unconnected_terminal_access_terminal_names": [ getattr(obj, "Name", "") for obj in selected_terminals ], "selected_unconnected_terminal_access_device_names": [ getattr(obj, "Name", "") for obj in selected_devices ], + "selected_unconnected_terminal_access_carrier_names": [ + getattr(obj, "Name", "") for obj in selected_access_carriers + ], + "selected_unconnected_terminal_access_nearest_path_names": [ + getattr(obj, "Name", "") for obj in selected_nearest_paths + ], "missing_unconnected_terminal_access_refs": missing_refs, "max_unconnected_terminal_access_distance_mm": float(max_distance), } @@ -2172,7 +2221,29 @@ class AutoRoutingController: return None selected = [] + selected_names = set() + selected_access_carriers = [] + selected_access_carrier_names = set() + selected_targets = [] + selected_target_names = set() missing_refs = [] + + def add_selection_object(obj): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") or "" + if obj_name in selected_names: + return True + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), obj_name) + except Exception: + return False + selected_names.add(obj_name) + return True + try: Gui.Selection.clearSelection() except Exception: @@ -2186,17 +2257,39 @@ class AutoRoutingController: str(sample.get("terminal_uuid", "") or sample.get("name", "") or sample.get("label", "") or "").strip() ) continue - try: - Gui.Selection.addSelection(obj) - except Exception: - try: - Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(obj, "Name", "")) - except Exception: - continue + if not add_selection_object(obj): + continue selected.append(obj) + + access_name = str(sample.get("access_carrier", "") or "").strip() + access_obj = doc.getObject(access_name) if access_name else None + if access_obj is not None and add_selection_object(access_obj): + access_obj_name = getattr(access_obj, "Name", "") or "" + if access_obj_name not in selected_access_carrier_names: + selected_access_carrier_names.add(access_obj_name) + selected_access_carriers.append(access_obj) + + target_obj = self._find_object_by_name_or_label( + doc, + str(sample.get("target_name", "") or "").strip(), + str(sample.get("target_label", "") or "").strip(), + ) + if target_obj is not None and add_selection_object(target_obj): + target_obj_name = getattr(target_obj, "Name", "") or "" + if target_obj_name not in selected_target_names: + selected_target_names.add(target_obj_name) + selected_targets.append(target_obj) self.last_report = { "selected_long_terminal_accesses": len(selected), "selected_long_terminal_names": [getattr(obj, "Name", "") for obj in selected], + "selected_long_terminal_access_carriers": len(selected_access_carriers), + "selected_long_terminal_access_carrier_names": [ + getattr(obj, "Name", "") for obj in selected_access_carriers + ], + "selected_long_terminal_access_targets": len(selected_targets), + "selected_long_terminal_access_target_names": [ + getattr(obj, "Name", "") for obj in selected_targets + ], "missing_long_terminal_refs": missing_refs, } return self.last_report @@ -2857,6 +2950,10 @@ class AutoRoutingController: total_suggestions = 0 total_duplicates = 0 total_stale = 0 + unconnected_targets = 0 + unconnected_created = 0 + unconnected_duplicates = 0 + unconnected_pair_labels = [] diagnostic_passes = 0 max_passes = 5 for _index in range(max_passes): @@ -2876,6 +2973,18 @@ class AutoRoutingController: total_suggestions += int(bridge_report.get("suggestions", 0) or 0) total_duplicates += int(bridge_report.get("duplicates", 0) or 0) total_stale += int(bridge_report.get("stale_suggestions", 0) or 0) + unconnected_targets += int( + bridge_report.get("unconnected_terminal_access_bridge_targets", 0) or 0 + ) + unconnected_created += int( + bridge_report.get("unconnected_terminal_access_user_path_bridges", 0) or 0 + ) + unconnected_duplicates += int( + bridge_report.get("unconnected_terminal_access_bridge_duplicates", 0) or 0 + ) + unconnected_pair_labels.extend( + list(bridge_report.get("unconnected_terminal_access_bridge_pair_labels", []) or []) + ) # 新桥接会改变路径组件关系;继续诊断一轮,处理链式接入建议。 if created_count <= 0: break @@ -2889,6 +2998,10 @@ class AutoRoutingController: "duplicate_bridges": total_duplicates, "stale_suggestions": total_stale, "diagnostic_passes": diagnostic_passes, + "unconnected_terminal_access_bridge_targets": unconnected_targets, + "unconnected_terminal_access_user_path_bridges": unconnected_created, + "unconnected_terminal_access_bridge_duplicates": unconnected_duplicates, + "unconnected_terminal_access_bridge_pair_labels": unconnected_pair_labels, "main_path_detour_bridge_pairs": int(detour_bridge_report.get("main_path_detour_bridge_pairs", 0) or 0), "main_path_detour_user_path_bridges": detour_created, "main_path_detour_bridge_duplicates": int(detour_bridge_report.get("main_path_detour_bridge_duplicates", 0) or 0), @@ -3596,6 +3709,12 @@ class AutoRoutingTaskPanel: "汇总预检、路径网络和批量布线的最新诊断对象,便于手动测试后统一复盘。", ) + self.acceptance_view_button = _style_command_button( + QtWidgets.QPushButton(), + "整理验收视图", + "不重新布线,只隐藏 route carrier 辅助对象,并显示/重刷 04_Routed 导线和样式。", + ) + self.clear_routes_button = _style_command_button(QtWidgets.QPushButton(), "清除布线连接") self.clear_carriers_button = _style_command_button(QtWidgets.QPushButton(), "清除走线路径") self.save_button = _style_command_button(QtWidgets.QPushButton(), "保存") @@ -3650,6 +3769,7 @@ class AutoRoutingTaskPanel: self.check_readiness_button, self.route_connections_button, self.diagnostic_summary_button, + self.acceptance_view_button, self.clear_routes_button, self.clear_carriers_button, self.save_button, @@ -3728,6 +3848,7 @@ class AutoRoutingTaskPanel: self.generate_layout_button.clicked.connect(self.generate_layout_space) self.route_connections_button.clicked.connect(self.route_eplan_connections) self.diagnostic_summary_button.clicked.connect(self.collect_routing_diagnostic_summary) + self.acceptance_view_button.clicked.connect(self.apply_phase1_acceptance_view) self.clear_routes_button.clicked.connect(self.clear_routing_connections) self.clear_carriers_button.clicked.connect(self.clear_route_carriers) self.save_button.clicked.connect(self.save) @@ -3993,6 +4114,9 @@ class AutoRoutingTaskPanel: terminal_fallback_created = result.get("terminal_access_fallback_user_path_bridges", 0) terminal_fallback_duplicates = result.get("terminal_access_fallback_bridge_duplicates", 0) missing_terminal_fallback_refs = list(result.get("missing_terminal_access_fallback_bridge_refs", []) or []) + unconnected_targets = result.get("unconnected_terminal_access_bridge_targets", 0) + unconnected_created = result.get("unconnected_terminal_access_user_path_bridges", 0) + unconnected_duplicates = result.get("unconnected_terminal_access_bridge_duplicates", 0) if created <= 0: detour_text = "" if detour_pairs: @@ -4006,6 +4130,12 @@ class AutoRoutingTaskPanel: int(terminal_fallback_targets or 0), int(terminal_fallback_duplicates or 0), ) + unconnected_text = "" + if unconnected_targets: + unconnected_text = " 未接入端子接入段 {0} 个,已存在 {1} 条。".format( + int(unconnected_targets or 0), + int(unconnected_duplicates or 0), + ) missing_text = "" if missing_detour_pairs: missing_text = " 未找到配对:{0}。".format("、".join(missing_detour_pairs[:3])) @@ -4014,12 +4144,13 @@ class AutoRoutingTaskPanel: "、".join(missing_terminal_fallback_refs[:3]) ) self._set_status( - "未按诊断建议生成桥接。建议 {0} 条,已存在 {1} 条,失效 {2} 条。{3}{4}{5}请先点击“检查布线路径网络”或“汇总布线诊断”确认是否存在可桥接建议。{6}".format( + "未按诊断建议生成桥接。建议 {0} 条,已存在 {1} 条,失效 {2} 条。{3}{4}{5}{6}请先点击“检查布线路径网络”或“汇总布线诊断”确认是否存在可桥接建议。{7}".format( suggestions, duplicates, stale, detour_text, terminal_fallback_text, + unconnected_text, missing_text, self.controller.summary(), ) @@ -4037,14 +4168,21 @@ class AutoRoutingTaskPanel: int(terminal_fallback_targets or 0), int(terminal_fallback_created or 0), ) + unconnected_text = "" + if unconnected_targets or unconnected_created: + unconnected_text = " 未接入端子接入段 {0} 个,生成 {1} 条。".format( + int(unconnected_targets or 0), + int(unconnected_created or 0), + ) self._set_status( - "已按诊断建议生成桥接 UserPath:{0} 条。建议 {1} 条,已存在 {2} 条,失效 {3} 条。{4}{5}请重新生成布线路径网络/布线连接验证效果。{6}".format( + "已按诊断建议生成桥接 UserPath:{0} 条。建议 {1} 条,已存在 {2} 条,失效 {3} 条。{4}{5}{6}请重新生成布线路径网络/布线连接验证效果。{7}".format( created, suggestions, duplicates, stale, detour_text, terminal_fallback_text, + unconnected_text, self.controller.summary(), ) ) @@ -4401,17 +4539,21 @@ class AutoRoutingTaskPanel: result = self.controller.select_unconnected_terminal_access_issues() terminals = result.get("selected_unconnected_terminal_access_terminals", 0) devices = result.get("selected_unconnected_terminal_access_devices", 0) + access_carriers = result.get("selected_unconnected_terminal_access_carriers", 0) + nearest_paths = result.get("selected_unconnected_terminal_access_nearest_paths", 0) max_distance = float(result.get("max_unconnected_terminal_access_distance_mm", 0.0) or 0.0) missing = list(result.get("missing_unconnected_terminal_access_refs", []) or []) - if terminals <= 0 and devices <= 0: + if terminals <= 0 and devices <= 0 and access_carriers <= 0 and nearest_paths <= 0: self._set_status( "未选择未接入端子。请先检查布线路径网络,确认存在 unconnected_terminals。" + self.controller.summary() ) return - message = "已选择未接入端子:端子 {0} 个,设备 {1} 个;最大最近网络距离 {2:.1f} mm。".format( + message = "已选择未接入端子:端子 {0} 个,设备 {1} 个,接入段 {2} 条,最近路径 {3} 条;最大最近网络距离 {4:.1f} mm。".format( terminals, devices, + access_carriers, + nearest_paths, max_distance, ) if missing: @@ -4537,6 +4679,8 @@ class AutoRoutingTaskPanel: try: result = self.controller.select_long_terminal_accesses() selected = result.get("selected_long_terminal_accesses", 0) + access_carriers = result.get("selected_long_terminal_access_carriers", 0) + targets = result.get("selected_long_terminal_access_targets", 0) missing = list(result.get("missing_long_terminal_refs", []) or []) if selected <= 0: self._set_status( @@ -4544,7 +4688,11 @@ class AutoRoutingTaskPanel: + self.controller.summary() ) return - message = "已选择长接入端子:{0} 个。".format(selected) + message = "已选择长接入端子:{0} 个,接入段 {1} 条,目标路径 {2} 条。".format( + selected, + int(access_carriers or 0), + int(targets or 0), + ) if missing: message += " 未找到:{0}。".format("、".join(missing[:5])) message += "请检查端子位置、设备装配高度,或为设备补局部出线路径。" @@ -4825,6 +4973,25 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def apply_phase1_acceptance_view(self): + try: + report = self.controller.apply_phase1_acceptance_view() + routed = report.get("routed_wire_visibility", {}) if isinstance(report, dict) else {} + carriers = report.get("route_carrier_visibility", {}) if isinstance(report, dict) else {} + styles = report.get("wire_style_application", {}) if isinstance(report, dict) else {} + self._set_status( + "已整理验收视图:显示导线 {0}/{1} 条,隐藏辅助路径 {2}/{3} 条,导线样式已应用 {4}/{5} 条。".format( + routed.get("visible", 0), + routed.get("routed", 0), + report.get("hidden_route_carriers", 0), + carriers.get("total", 0), + styles.get("applied", 0), + styles.get("expected", 0), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + def clear_routing_connections(self): try: removed = self.controller.clear_routing_connections() diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index 2ed4646..75c5a4e 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -471,10 +471,17 @@ def _find_device_group(doc, element_uuid): preferred_name = DEVICE_GROUP_PREFIX + _safe_token(target_uuid) obj = doc.getObject(preferred_name) - if obj is not None: + if obj is not None and getattr(obj, "Name", "").startswith(DEVICE_GROUP_PREFIX): return obj for candidate in doc.Objects: + if not getattr(candidate, "Name", "").startswith(DEVICE_GROUP_PREFIX): + continue + try: + if not candidate.isDerivedFrom("App::DocumentObjectGroup"): + continue + except Exception: + continue if "QetElementUuid" in getattr(candidate, "PropertiesList", []): if getattr(candidate, "QetElementUuid", "").strip() == target_uuid: return candidate @@ -1224,6 +1231,11 @@ def _is_exchange_sidecar_group(obj): child_name = _object_name(obj) if child_name.startswith(TERMINAL_GROUP_PREFIX) or child_name.startswith(WIRE_GROUP_PREFIX): return True + try: + if TerminalObjects.is_terminal_hint_object(obj): + return True + except Exception: + pass return getattr(obj, "QetGroupKind", "").strip() in {GROUP_KIND_TERMINALS, GROUP_KIND_WIRES} diff --git a/src/Mod/FreeCADExchange/ExchangeWriteBack.py b/src/Mod/FreeCADExchange/ExchangeWriteBack.py index 89e455e..7b5bbe1 100644 --- a/src/Mod/FreeCADExchange/ExchangeWriteBack.py +++ b/src/Mod/FreeCADExchange/ExchangeWriteBack.py @@ -5,12 +5,18 @@ import os from datetime import datetime from pathlib import Path import traceback +import uuid import FreeCAD as App import DeviceImport import TerminalObjects as TerminalObjects +try: + import TerminalImport +except ImportError: + TerminalImport = None + try: import FreeCADGui as Gui except ImportError: @@ -140,6 +146,68 @@ def _output_path_for_exchange_json(): return str(Path(json_path).with_name("3d_to_2d.json")) +def _input_path_for_scene(scene_path): + scene_path = (scene_path or "").strip() + if not scene_path: + return "" + path = Path(scene_path) + if path.suffix.lower() == ".fcstd": + return str(path.with_name("2d_to_3d.json")) + if path.is_dir(): + return str(path / "2d_to_3d.json") + return str(path.parent / "2d_to_3d.json") + + +def _load_json_payload(path): + path_text = (path or "").strip() + if not path_text: + return None + try: + candidate = Path(path_text) + if not candidate.is_file(): + return None + return json.loads(candidate.read_text(encoding="utf-8")) + except Exception as exc: + _append_debug_log("write-back could not load payload {0}: {1}".format(path_text, exc)) + return None + + +def _payload_for_writeback(scene_path, payload=None): + if isinstance(payload, dict): + return payload + + env_path = os.environ.get(ENV_JSON_PATH, "").strip() + loaded = _load_json_payload(env_path) + if isinstance(loaded, dict): + return loaded + + loaded = _load_json_payload(_input_path_for_scene(scene_path)) + if isinstance(loaded, dict): + return loaded + + return payload + + +def _sync_terminals_for_writeback(doc, scene_path, payload): + if TerminalImport is None or not isinstance(payload, dict): + return None + if not isinstance(payload.get("devices"), list) or not payload.get("devices"): + return None + try: + # 保存/写回以当前 2d_to_3d.json 为端子快照,先同步 3D 工程端子,避免旧工程继续回写缺失或重复端子。 + return TerminalImport.import_terminals_from_payload(payload, scene_path) + except Exception as exc: + _append_debug_log("write-back terminal sync failed: {0}".format(exc)) + _append_debug_log(traceback.format_exc()) + return None + + +def sync_terminals_from_current_payload(doc, scene_path="", payload=None): + scene_path = _scene_path_from_doc(doc, scene_path) + payload = _payload_for_writeback(scene_path, payload) + return _sync_terminals_for_writeback(doc, scene_path, payload) + + def _format_timestamp(): return datetime.now().astimezone().isoformat(timespec="seconds") @@ -173,9 +241,59 @@ def _collect_instance_bindings(doc): return bindings +def _stable_terminal_instance_id(project_uuid, device_instance_id, terminal_obj): + values = [ + project_uuid, + device_instance_id, + getattr(terminal_obj, "QetElementUuid", "").strip(), + getattr(terminal_obj, "QetTerminalUuid", "").strip(), + getattr(terminal_obj, "QetTemplateSlotName", "").strip(), + getattr(terminal_obj, "Label", "").strip(), + getattr(terminal_obj, "Name", "").strip(), + ] + seed = "qet-freecad-writeback-terminal|" + "|".join(values) + return str(uuid.uuid5(uuid.NAMESPACE_URL, seed)) + + +def _writeback_terminal_instance_id(project_uuid, terminal_obj, device_instance_id, used_ids): + terminal_instance_id = ( + getattr(terminal_obj, "QetTerminalInstanceId", "").strip() + or getattr(terminal_obj, "QetInstanceId", "").strip() + or "" + ) + if ( + not terminal_instance_id + or terminal_instance_id == device_instance_id + or terminal_instance_id in used_ids + ): + terminal_instance_id = _stable_terminal_instance_id( + project_uuid, + device_instance_id, + terminal_obj, + ) + suffix = 1 + while terminal_instance_id in used_ids: + terminal_instance_id = str(uuid.uuid5( + uuid.NAMESPACE_URL, + "{0}|{1}".format(terminal_instance_id, suffix), + )) + suffix += 1 + TerminalObjects.ensure_string_property( + terminal_obj, + "QetTerminalInstanceId", + "QET Exchange", + "Stable 3D terminal instance UUID", + terminal_instance_id, + ) + used_ids.add(terminal_instance_id) + return terminal_instance_id + + def _collect_terminal_bindings(doc): bindings = [] seen = set() + used_terminal_instance_ids = set() + project_uuid = _project_uuid_from_doc(doc) for device_group in _iter_device_groups(doc): instance_id = getattr(device_group, "QetInstanceId", "").strip() for terminal_obj in _iter_terminal_objects(device_group): @@ -186,7 +304,12 @@ def _collect_terminal_bindings(doc): or binding_mode == TerminalObjects.TERMINAL_BINDING_MODE_LOCAL ): continue - terminal_instance_id = getattr(terminal_obj, "QetInstanceId", "").strip() or instance_id + terminal_instance_id = _writeback_terminal_instance_id( + project_uuid, + terminal_obj, + instance_id, + used_terminal_instance_ids, + ) if not terminal_uuid or not terminal_instance_id: continue key = (terminal_uuid, terminal_instance_id) @@ -235,6 +358,9 @@ def write_back_document(doc=None, scene_path="", payload=None): "Cannot determine the 3d_to_2d.json output path." ) + payload = _payload_for_writeback(scene_path, payload) + _sync_terminals_for_writeback(doc, scene_path, payload) + project_uuid = _project_uuid_from_doc(doc, payload) if not project_uuid: raise ExchangeWriteBackError( @@ -343,6 +469,19 @@ class _WriteBackObserver: _is_exchange_document(doc), ) ) + if not _is_exchange_document(doc): + return + try: + sync_terminals_from_current_payload(doc, scene_path=name) + except Exception as exc: + _append_debug_log("write-back terminal sync before save failed: {0}".format(exc)) + _append_debug_log(traceback.format_exc()) + try: + App.Console.PrintError( + "[FreeCADExchange] terminal sync before save failed: {0}\n".format(exc) + ) + except Exception: + pass def slotFinishSaveDocument(self, doc, name): _append_debug_log( diff --git a/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py b/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py index f09a305..d45263b 100644 --- a/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py +++ b/src/Mod/FreeCADExchange/PendingDeviceAssemblyPanel.py @@ -24,8 +24,24 @@ except ImportError: import DeviceImport +try: + import TerminalImport +except ImportError: + TerminalImport = None + +try: + import TemplateInstantiation +except ImportError: + TemplateInstantiation = None + +try: + import BatchAssembly +except ImportError: + BatchAssembly = None + COMMAND_NAME = "QET_Exchange_OpenPendingDevicePanel" +STATE_PAYLOAD = "_qet_exchange_payload" class PendingDeviceAssemblyPanelError(RuntimeError): @@ -169,6 +185,227 @@ def _set_status(label, message, error=False): pass +def _imported_model_objects(result): + objects = [] + for obj in list((result or {}).get("imported_objects", []) or []): + if obj is not None and getattr(obj, "Name", ""): + objects.append(obj) + return objects + + +def focus_inserted_result(doc, result): + imported_objects = _imported_model_objects(result) + if not imported_objects: + raise PendingDeviceAssemblyPanelError( + "没有导入任何可显示模型对象,请检查该设备绑定的 3D 模型文件。" + ) + + device = (result or {}).get("device") + for obj in [device] + imported_objects: + view_object = getattr(obj, "ViewObject", None) + if view_object is not None: + try: + view_object.Visibility = True + except Exception: + pass + + if Gui is not None and hasattr(Gui, "Selection"): + try: + Gui.Selection.clearSelection() + except Exception: + pass + for obj in imported_objects: + try: + Gui.Selection.addSelection(doc.Name, obj.Name) + except Exception: + pass + + if Gui is not None and hasattr(Gui, "SendMsgToActiveView"): + try: + Gui.SendMsgToActiveView("ViewSelection") + except Exception: + try: + Gui.SendMsgToActiveView("ViewFit") + except Exception: + pass + + +def _sync_engineering_terminals_for_inserted_device(doc, result): + device = result.get("device") if isinstance(result, dict) else None + payload = getattr(App, STATE_PAYLOAD, None) + if TerminalImport is not None and isinstance(payload, dict): + project_uuid = (payload.get("project_uuid") or "").strip() + if project_uuid: + return TerminalImport.import_terminals_from_payload(payload, "") + + if TemplateInstantiation is not None and device is not None: + return TemplateInstantiation.ensure_engineering_terminals_for_device(doc, device) + + return { + "imported_terminals": 0, + "created_terminals": 0, + "updated_terminals": 0, + "warnings": ["端子同步模块不可用,已跳过工程端子生成。"], + } + + +def insert_device_and_sync_terminals(doc, device, **insert_kwargs): + result = DeviceImport.insert_pending_device(doc, device, **insert_kwargs) + result["terminal_report"] = _sync_engineering_terminals_for_inserted_device(doc, result) + return result + + +def _terminal_report_count(report, *keys): + for key in keys: + try: + return int(report.get(key, 0) or 0) + except Exception: + continue + return 0 + + +def _terminal_status_suffix(result): + report = result.get("terminal_report", {}) if isinstance(result, dict) else {} + created = _terminal_report_count(report, "imported_terminals", "created_terminals") + updated = _terminal_report_count(report, "updated_terminals") + skipped = sum( + _terminal_report_count(report, key) + for key in ( + "skipped_missing_slot", + "skipped_devices_without_template_slots", + "skipped_unbound_slots", + "skipped_missing_device", + ) + ) + return ",工程端子新增 {0} 个,更新 {1} 个,跳过 {2} 个".format( + created, + updated, + skipped, + ) + + +def _pending_batch_key(device): + if BatchAssembly is not None: + try: + strip_name, order = BatchAssembly._parse_strip_name_and_order(device) + if strip_name: + return strip_name, order + except Exception: + pass + text = ( + getattr(device, "QetDisplayTag", "") + or getattr(device, "Label", "") + or getattr(device, "Name", "") + or "" + ).strip() + prefix = "" + number = None + for index, char in enumerate(text): + if char.isdigit(): + prefix = text[:index].strip(" ::_-") + try: + number = int("".join(ch for ch in text[index:] if ch.isdigit()) or "0") + except Exception: + number = None + break + return prefix or text, number + + +def matching_pending_batch_devices(doc, seed_device): + seed_prefix, _seed_order = _pending_batch_key(seed_device) + seed_prefix_key = (seed_prefix or "").strip().lower() + if not seed_prefix_key: + return [seed_device] + + matches = [] + for item in DeviceImport.list_pending_devices(doc): + device = item.get("device") + if device is None: + continue + prefix, order = _pending_batch_key(device) + if (prefix or "").strip().lower() == seed_prefix_key: + matches.append((order if order is not None else 10**9, getattr(device, "Label", "") or getattr(device, "Name", ""), device)) + matches.sort(key=lambda item: (item[0], item[1])) + return [device for _order, _label, device in matches] or [seed_device] + + +def _sync_engineering_terminals_after_batch(doc, devices): + payload = getattr(App, STATE_PAYLOAD, None) + if TerminalImport is not None and isinstance(payload, dict) and (payload.get("project_uuid") or "").strip(): + return TerminalImport.import_terminals_from_payload(payload, "") + + totals = {"imported_terminals": 0, "created_terminals": 0, "updated_terminals": 0, "warnings": []} + if TemplateInstantiation is None: + totals["warnings"].append("端子同步模块不可用,已跳过工程端子生成。") + return totals + for device in devices: + report = TemplateInstantiation.ensure_engineering_terminals_for_device(doc, device) + for key in ("imported_terminals", "created_terminals", "updated_terminals"): + try: + totals[key] += int(report.get(key, 0) or 0) + except Exception: + pass + totals["warnings"].extend(list(report.get("warnings", []) or [])) + return totals + + +def insert_matching_pending_batch_to_target( + doc, + seed_device, + target, + pitch_mm=5.2, + start_offset_mm=0.0, + mount_offset_mm=20.0, +): + if BatchAssembly is None: + raise PendingDeviceAssemblyPanelError("批量排布模块不可用。") + if target is None: + raise PendingDeviceAssemblyPanelError("请先在 3D 视图中选择导轨或安装目标。") + rail = BatchAssembly._ensure_rail(target) + devices = matching_pending_batch_devices(doc, seed_device) + batch_prefix, _seed_order = _pending_batch_key(seed_device) + base = BatchAssembly._base_point(rail) + axis = BatchAssembly._axis_vector(rail) + source_doc_cache = {} + results = [] + for index, device in enumerate(devices): + point = BatchAssembly._point_at( + base, + axis, + float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0), + ) + result = DeviceImport.insert_pending_device( + doc, + device, + source_doc_cache=source_doc_cache, + mount_target=rail, + mount_placement=BatchAssembly._placement_at(rail, point), + mount_offset_mm=mount_offset_mm, + ) + results.append(result) + + terminal_report = _sync_engineering_terminals_after_batch(doc, [result["device"] for result in results]) + for result in results: + result["terminal_report"] = terminal_report + try: + doc.recompute() + except Exception: + pass + return { + "devices": [result["device"] for result in results], + "results": results, + "terminal_report": terminal_report, + "batch_prefix": batch_prefix, + "device_labels": [ + getattr(result["device"], "QetDisplayTag", "") + or getattr(result["device"], "Label", "") + or getattr(result["device"], "Name", "") + for result in results + ], + "inserted_count": len(results), + } + + class PendingDeviceAssemblyTaskPanel: def __init__(self): if QtWidgets is None: @@ -185,6 +422,7 @@ class PendingDeviceAssemblyTaskPanel: self.refresh_button = QtWidgets.QPushButton("刷新清单") self.insert_button = QtWidgets.QPushButton("插入设备") self.insert_to_target_button = QtWidgets.QPushButton("插入到选中目标") + self.batch_insert_to_target_button = QtWidgets.QPushButton("批量插入同组到选中目标") layout.addWidget(self.refresh_button) layout.addWidget(self.insert_button) @@ -195,11 +433,31 @@ class PendingDeviceAssemblyTaskPanel: self.mount_offset_input.setDecimals(1) self.mount_offset_input.setSingleStep(1.0) self.mount_offset_input.setSuffix(" mm") - self.mount_offset_input.setValue(0.0) + self.mount_offset_input.setValue(20.0) offset_row.addWidget(self.mount_offset_input) layout.addLayout(offset_row) + batch_row = QtWidgets.QHBoxLayout() + batch_row.addWidget(QtWidgets.QLabel("批量间距")) + self.batch_pitch_input = QtWidgets.QDoubleSpinBox() + self.batch_pitch_input.setRange(0.1, 10000.0) + self.batch_pitch_input.setDecimals(1) + self.batch_pitch_input.setSingleStep(1.0) + self.batch_pitch_input.setSuffix(" mm") + self.batch_pitch_input.setValue(5.2) + batch_row.addWidget(self.batch_pitch_input) + batch_row.addWidget(QtWidgets.QLabel("起点偏移")) + self.batch_start_offset_input = QtWidgets.QDoubleSpinBox() + self.batch_start_offset_input.setRange(-10000.0, 10000.0) + self.batch_start_offset_input.setDecimals(1) + self.batch_start_offset_input.setSingleStep(1.0) + self.batch_start_offset_input.setSuffix(" mm") + self.batch_start_offset_input.setValue(0.0) + batch_row.addWidget(self.batch_start_offset_input) + layout.addLayout(batch_row) + layout.addWidget(self.insert_to_target_button) + layout.addWidget(self.batch_insert_to_target_button) self.status_label = QtWidgets.QLabel("") self.status_label.setWordWrap(True) @@ -208,6 +466,7 @@ class PendingDeviceAssemblyTaskPanel: self.refresh_button.clicked.connect(self.refresh) self.insert_button.clicked.connect(self.insert_selected_device) self.insert_to_target_button.clicked.connect(self.insert_selected_device_to_target) + self.batch_insert_to_target_button.clicked.connect(self.insert_matching_batch_to_target) self.refresh() @@ -237,35 +496,80 @@ class PendingDeviceAssemblyTaskPanel: def insert_selected_device(self): try: - result = DeviceImport.insert_pending_device(_document(), self._selected_device()) + doc = _document() + result = insert_device_and_sync_terminals(doc, self._selected_device()) + focus_inserted_result(doc, result) self.refresh() _set_status( self.status_label, - "已插入设备:{0}".format(getattr(result["device"], "Label", "") or getattr(result["device"], "Name", "")), + "已插入设备:{0},导入模型对象 {1} 个{2}".format( + getattr(result["device"], "Label", "") or getattr(result["device"], "Name", ""), + len(_imported_model_objects(result)), + _terminal_status_suffix(result), + ), ) except Exception as exc: _set_status(self.status_label, str(exc), error=True) def insert_selected_device_to_target(self): try: + doc = _document() device = self._selected_device() context = selected_mount_context(exclude_device=device) target = context.get("target") if target is None: raise PendingDeviceAssemblyPanelError("请先在 3D 视图中选择安装板、导轨、线槽或柜体安装面。") - result = DeviceImport.insert_pending_device( - _document(), + result = insert_device_and_sync_terminals( + doc, device, mount_target=target, mount_placement=context.get("placement"), mount_normal=context.get("normal"), mount_offset_mm=self.mount_offset_input.value(), ) + focus_inserted_result(doc, result) + self.refresh() + _set_status( + self.status_label, + "已插入设备到选中目标:{0},导入模型对象 {1} 个{2}".format( + getattr(result["device"], "Label", "") or getattr(result["device"], "Name", ""), + len(_imported_model_objects(result)), + _terminal_status_suffix(result), + ), + ) + except Exception as exc: + _set_status(self.status_label, str(exc), error=True) + + def insert_matching_batch_to_target(self): + try: + doc = _document() + device = self._selected_device() + context = selected_mount_context(exclude_device=device) + target = context.get("target") + report = insert_matching_pending_batch_to_target( + doc, + device, + target, + pitch_mm=self.batch_pitch_input.value(), + start_offset_mm=self.batch_start_offset_input.value(), + mount_offset_mm=self.mount_offset_input.value(), + ) + imported_count = sum(len(_imported_model_objects(result)) for result in report.get("results", [])) + device_labels = list(report.get("device_labels", []) or []) + label_preview = "、".join(str(label) for label in device_labels[:5] if str(label)) + if len(device_labels) > 5: + label_preview += " 等" + if label_preview: + label_preview = "({0})".format(label_preview) self.refresh() _set_status( self.status_label, - "已插入设备到选中目标:{0}".format( - getattr(result["device"], "Label", "") or getattr(result["device"], "Name", "") + "已批量插入同组 {0} 待装配设备 {1} 个{2},导入模型对象 {3} 个{4}".format( + report.get("batch_prefix", "") or "设备", + len(report.get("devices", []) or []), + label_preview, + imported_count, + _terminal_status_suffix({"terminal_report": report.get("terminal_report", {})}), ), ) except Exception as exc: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index ea9c8a4..5aa8365 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -140,7 +140,8 @@ DEFAULT_KIND_COST_FACTORS = { ROUTE_CARRIER_KIND: 1.0, ROUTE_CARRIER_KIND_AUXILIARY_PATH: 2.0, ROUTE_CARRIER_KIND_TERMINAL_ACCESS: 2.0, - ROUTE_CARRIER_KIND_ROUTING_RANGE: 40.0, + # RoutingRange 是安装板/柜内面域兜底;成本要足够高,避免线槽复用较多时反向抢主路径。 + ROUTE_CARRIER_KIND_ROUTING_RANGE: 1000.0, ROUTE_CARRIER_KIND_USER_PATH: 1.0, } ROUTE_CARRIER_VIEW_STYLES = { @@ -2468,6 +2469,10 @@ def create_user_path_bridges_from_diagnostic_suggestions(doc, diagnostic, projec "created": [], "duplicates": 0, "stale_suggestions": 0, + "unconnected_terminal_access_bridge_targets": 0, + "unconnected_terminal_access_user_path_bridges": 0, + "unconnected_terminal_access_bridge_duplicates": 0, + "unconnected_terminal_access_bridge_pair_labels": [], } if doc is None or not isinstance(diagnostic, dict): return report @@ -2517,6 +2522,41 @@ def create_user_path_bridges_from_diagnostic_suggestions(doc, diagnostic, projec ), ) report["created"].append(bridge) + + for item in diagnostic.get("unconnected_terminals", []) or []: + if not isinstance(item, dict): + continue + access_name = str(item.get("access_carrier", "") or "").strip() + nearest_name = str(item.get("nearest_network_carrier_name", "") or "").strip() + nearest_label = str(item.get("nearest_network_carrier_label", "") or "").strip() + if not access_name or not (nearest_name or nearest_label): + continue + report["suggestions"] += 1 + report["unconnected_terminal_access_bridge_targets"] += 1 + access_carrier = _document_object_by_name(doc, access_name) + nearest_carrier = _document_object_by_name(doc, nearest_name) + if nearest_carrier is None and nearest_label: + nearest_carrier = _document_object_by_label(doc, nearest_label) + if not is_route_carrier(access_carrier) or not is_route_carrier(nearest_carrier): + report["stale_suggestions"] += 1 + continue + bridges = create_user_path_bridge_between_objects( + doc, + access_carrier, + nearest_carrier, + project_uuid=project_uuid, + bridge_kind="UnconnectedTerminalAccessBridge", + ) + if bridges: + report["created"].extend(bridges) + report["unconnected_terminal_access_user_path_bridges"] += len(bridges) + for bridge in bridges: + pair_label = str(getattr(bridge, "QetRouteBridgePairLabel", "") or "").strip() + if pair_label: + report["unconnected_terminal_access_bridge_pair_labels"].append(pair_label) + else: + report["duplicates"] += 1 + report["unconnected_terminal_access_bridge_duplicates"] += 1 return report @@ -2961,6 +3001,16 @@ def _document_object_by_name(doc, name): return None +def _document_object_by_label(doc, label): + if doc is None or not label: + return None + label = str(label or "").strip() + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == label: + return candidate + return None + + def cleanup_invalid_source_carriers(doc): """Remove generated carriers whose FreeCAD source object is missing or invalid.""" if doc is None: @@ -3137,7 +3187,7 @@ def routing_source_summary(doc): } -def prepare_layout_space_sources_from_document(doc, project_uuid=""): +def prepare_layout_space_sources_from_document(doc, project_uuid="", include_existing_network=True): """Normalize the current FreeCAD document as an EPLAN-style layout space. This does not generate the routing path network. It marks source objects so @@ -3174,13 +3224,15 @@ def prepare_layout_space_sources_from_document(doc, project_uuid=""): except Exception: pass - return { + payload = { "wire_duct_sources": len(wire_duct_sources), "support_surface_sources": len(support_surface_sources), "wiring_cut_out_sources": len(wiring_cut_out_sources), "routable_terminals": len(_collect_routable_terminals(doc)), - "existing_network": network_summary(doc), } + if include_existing_network: + payload["existing_network"] = network_summary(doc) + return payload def create_wire_duct_carriers_from_document( @@ -3482,7 +3534,32 @@ def _terminal_local_route_issue(terminal): continue points = [_json_route_point(item) for item in point_items if item is not None] valid_points = [point for point in points if point is not None] - if len(_normalized_route_points(valid_points)) >= 2: + normalized_points = _normalized_route_points(valid_points) + if len(normalized_points) >= 2: + try: + origin = _vector(TerminalObjects.terminal_origin(terminal)) + global_points = [_terminal_local_point_to_global(terminal, point) for point in normalized_points] + bbox = _terminal_parent_device_bbox(terminal, origin) + except Exception: + bbox = None + global_points = [] + if ( + bbox is not None + and global_points + and _point_inside_bbox(global_points[-1], bbox, tolerance=DEFAULT_NODE_TOLERANCE) + ): + invalid_samples.append( + { + "property_name": property_name, + "reason": "local_route_end_inside_device_bbox", + "message": "Local route endpoint is still inside the parent device bounding box.", + "raw_sample": raw[:160], + "valid_point_count": len(valid_points), + "local_route_end_point": _point_payload(global_points[-1]), + "endpoint_device_bbox": _bbox_payload(bbox, clearance=0.0), + } + ) + continue return None invalid_samples.append( { @@ -3796,7 +3873,7 @@ def terminal_access_diagnostics(terminal, exit_length=20.0, max_exit_length=None if not points or _distance(points[0], origin) > DEFAULT_NODE_TOLERANCE: points.insert(0, origin) points = _normalized_route_points(points) - if len(points) >= 2: + if len(points) >= 2 and not _terminal_local_route_end_inside_parent_device(terminal, points): direction = _normalize(_subtract(points[1], points[0])) or App.Vector(0, 0, 1) return { "requested_exit_length_mm": max(float(exit_length or 0.0), 0.0), @@ -4033,13 +4110,27 @@ def terminal_access_path_points(terminal, exit_length=20.0, max_exit_length=None if not points or _distance(points[0], origin) > DEFAULT_NODE_TOLERANCE: points.insert(0, origin) normalized = _normalized_route_points(points) - if len(normalized) >= 2: + if len(normalized) >= 2 and not _terminal_local_route_end_inside_parent_device(terminal, normalized): return normalized return _normalized_route_points( [origin, _terminal_device_aware_exit_point(terminal, exit_length, max_exit_length=max_exit_length)] ) +def _terminal_local_route_end_inside_parent_device(terminal, global_points): + try: + origin = _vector(TerminalObjects.terminal_origin(terminal)) + bbox = _terminal_parent_device_bbox(terminal, origin) + except Exception: + bbox = None + normalized = _normalized_route_points(global_points) + return bool( + bbox is not None + and normalized + and _point_inside_bbox(normalized[-1], bbox, tolerance=DEFAULT_NODE_TOLERANCE) + ) + + def terminal_access_carrier_for_terminal(terminal): doc = getattr(terminal, "Document", None) carrier = _live_source_carrier(doc, terminal) @@ -4407,9 +4498,52 @@ def _terminal_access_target_candidate(network, exit_point, max_distance): selected = dict(ranked[0]) selected["terminal_access_target_rule"] = "fallback_only" selected["terminal_access_fallback_target"] = True + selected["terminal_access_max_distance_mm"] = float(max(float(max_distance or 0.0), 0.0)) + nearest_main_path = _nearest_terminal_access_main_path_candidate(network, exit_point) + if nearest_main_path is not None: + selected.update(_terminal_access_main_path_summary(nearest_main_path, max_distance)) return selected +def _nearest_terminal_access_main_path_candidate(network, exit_point): + candidates = connection_point_candidates( + network, + exit_point, + limit=0, + max_distance=0.0, + ) + main_path_candidates = [ + candidate + for candidate in candidates + if _is_terminal_access_main_path_target(candidate.get("carrier")) + ] + if not main_path_candidates: + return None + return min( + main_path_candidates, + key=lambda candidate: float(candidate.get("distance", 0.0) or 0.0), + ) + + +def _terminal_access_main_path_summary(candidate, max_distance): + carrier = candidate.get("carrier") + distance = float(candidate.get("distance", 0.0) or 0.0) + max_distance_value = float(max(float(max_distance or 0.0), 0.0)) + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + name = (getattr(carrier, "Name", "") or "").strip() + label = (getattr(carrier, "Label", "") or "").strip() or name + return { + "nearest_main_path_kind": kind, + "nearest_main_path_name": name, + "nearest_main_path_label": label, + "nearest_main_path_distance_mm": distance, + "nearest_main_path_point": _point_payload(_vector(candidate.get("point"))), + "nearest_main_path_over_max_distance": bool( + max_distance_value > 0.0 and distance > max_distance_value + ), + } + + def _set_terminal_access_target_metadata(carrier, candidate): if carrier is None or not isinstance(candidate, dict): return @@ -4452,12 +4586,52 @@ def _set_terminal_access_target_metadata(carrier, candidate): "Whether the terminal access target is only a fallback carrier", "1" if bool(candidate.get("terminal_access_fallback_target", False)) else "0", ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessNearestMainPathKind", + PROPERTY_GROUP, + "Nearest main path carrier kind when TerminalAccess falls back to a routing range", + str(candidate.get("nearest_main_path_kind", "") or ""), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessNearestMainPathName", + PROPERTY_GROUP, + "Nearest main path carrier name when TerminalAccess falls back to a routing range", + str(candidate.get("nearest_main_path_name", "") or ""), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessNearestMainPathLabel", + PROPERTY_GROUP, + "Nearest main path carrier label when TerminalAccess falls back to a routing range", + str(candidate.get("nearest_main_path_label", "") or ""), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetTerminalAccessNearestMainPathOverMaxDistance", + PROPERTY_GROUP, + "Whether the nearest main path exceeded the configured terminal access max distance", + "1" if bool(candidate.get("nearest_main_path_over_max_distance", False)) else "0", + ) _ensure_float_property( carrier, "QetTerminalAccessTargetDistanceMm", "Distance from terminal local exit to selected access target", float(candidate.get("distance", 0.0) or 0.0), ) + _ensure_float_property( + carrier, + "QetTerminalAccessNearestMainPathDistanceMm", + "Distance from terminal local exit to the nearest main path when fallback is selected", + float(candidate.get("nearest_main_path_distance_mm", 0.0) or 0.0), + ) + _ensure_float_property( + carrier, + "QetTerminalAccessMaxDistanceMm", + "Configured maximum distance allowed for TerminalAccess", + float(candidate.get("terminal_access_max_distance_mm", 0.0) or 0.0), + ) _ensure_integer_property( carrier, "QetTerminalAccessTargetComponentPrimarySegments", @@ -4583,6 +4757,7 @@ def create_routing_path_network_from_document( layout_space = prepare_layout_space_sources_from_document( doc, project_uuid=project_uuid, + include_existing_network=False, ) selected_wire_ducts = [] selected_user_paths = [] @@ -5014,29 +5189,40 @@ def nearest_node(network, point): return best_key, best_distance -def nearest_point_on_network(network, point): - """Return the closest point on any route-network edge. +def _route_carrier_summary(carrier): + if carrier is None: + return { + "carrier_kind": "", + "carrier_name": "", + "carrier_label": "", + } + name = (getattr(carrier, "Name", "") or "").strip() + label = (getattr(carrier, "Label", "") or "").strip() or name + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() + return { + "carrier_kind": kind, + "carrier_name": name, + "carrier_label": label, + } - The point may lie in the middle of a carrier segment. If a TerminalAccess - carrier ends there, the next graph build will split the crossed segment at - that point and create an EPLAN-like jump-in routing point. - """ + +def _nearest_point_on_network_payload(network, point): + """Return closest route-network point and the carrier owning that segment.""" if not isinstance(network, dict): - return None, None + return None nodes = network.get("nodes", {}) or {} edges = network.get("edges", {}) or {} if not nodes or not edges: - return None, None + return None target = _vector(point) - best_point = None - best_distance = None + best_payload = None seen = set() for key, neighbors in edges.items(): start = nodes.get(key) if start is None: continue - for next_key, _weight, _carrier in neighbors: + for next_key, _weight, carrier in neighbors: pair = tuple(sorted((key, next_key))) if pair in seen: continue @@ -5046,12 +5232,39 @@ def nearest_point_on_network(network, point): continue candidate = _closest_point_on_segment(target, start, end) distance = _distance(target, candidate) - if best_distance is None or distance < best_distance: - best_point = candidate - best_distance = distance - if best_point is not None: - return best_point, best_distance - return nearest_node(network, target) + if best_payload is None or distance < best_payload["distance"]: + payload = { + "point": candidate, + "distance": float(distance), + } + payload.update(_route_carrier_summary(carrier)) + best_payload = payload + if best_payload is not None: + return best_payload + + node_key, distance = nearest_node(network, target) + if node_key is None: + return None + return { + "point": nodes.get(node_key), + "distance": float(distance), + "carrier_kind": "", + "carrier_name": "", + "carrier_label": "", + } + + +def nearest_point_on_network(network, point): + """Return the closest point on any route-network edge. + + The point may lie in the middle of a carrier segment. If a TerminalAccess + carrier ends there, the next graph build will split the crossed segment at + that point and create an EPLAN-like jump-in routing point. + """ + payload = _nearest_point_on_network_payload(network, point) + if payload is None: + return None, None + return payload.get("point"), payload.get("distance") def connection_point_candidates(network, point, limit=8, max_distance=0.0): @@ -5927,6 +6140,9 @@ def _terminal_for_access_carrier(carrier): def _terminal_access_diagnostic_payload(carrier): terminal = _terminal_for_access_carrier(carrier) access_points = _normalized_route_points(_carrier_points(carrier)) + endpoint_device_avoided = str( + getattr(carrier, "QetTerminalAccessAvoidedEndpointDevice", "") or "" + ).strip() == "1" payload = { "access_carrier_name": getattr(carrier, "Name", "") or "", "access_carrier_label": getattr(carrier, "Label", "") or "", @@ -5935,6 +6151,15 @@ def _terminal_access_diagnostic_payload(carrier): "target_label": (getattr(carrier, "QetTerminalAccessTargetLabel", "") or "").strip(), "target_rule": (getattr(carrier, "QetTerminalAccessTargetRule", "") or "").strip(), "target_distance_mm": float(getattr(carrier, "QetTerminalAccessTargetDistanceMm", 0.0) or 0.0), + "terminal_access_max_distance_mm": float(getattr(carrier, "QetTerminalAccessMaxDistanceMm", 0.0) or 0.0), + "nearest_main_path_kind": (getattr(carrier, "QetTerminalAccessNearestMainPathKind", "") or "").strip(), + "nearest_main_path_name": (getattr(carrier, "QetTerminalAccessNearestMainPathName", "") or "").strip(), + "nearest_main_path_label": (getattr(carrier, "QetTerminalAccessNearestMainPathLabel", "") or "").strip(), + "nearest_main_path_distance_mm": float(getattr(carrier, "QetTerminalAccessNearestMainPathDistanceMm", 0.0) or 0.0), + "nearest_main_path_over_max_distance": str( + getattr(carrier, "QetTerminalAccessNearestMainPathOverMaxDistance", "") or "" + ).strip() == "1", + "endpoint_device_avoided": endpoint_device_avoided, "access_length_mm": float(_route_length(access_points)), "access_points": [_point_payload(point) for point in access_points], } @@ -5952,6 +6177,13 @@ def _terminal_access_diagnostic_payload(carrier): "parent_device_element_uuid": terminal_payload.get("parent_device_element_uuid", ""), } ) + if endpoint_device_avoided: + try: + bbox = _terminal_parent_device_bbox(terminal, _vector(TerminalObjects.terminal_origin(terminal))) + except Exception: + bbox = None + if bbox is not None: + payload["endpoint_device_bbox"] = _bbox_payload(bbox, clearance=0.0) return payload @@ -6132,12 +6364,13 @@ def diagnose_routing_path_network( terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, terminal_access_warning_distance=0.0, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, + route_network=None, ): """Inspect the generated routing path network without routing wires.""" if doc is None: raise RoutingNetworkError("No FreeCAD document is available.") - network = build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance) + network = route_network if isinstance(route_network, dict) else build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance) components = _route_graph_components(network) summary = _network_summary_from_graph(network) isolated_components = _actionable_isolated_components(components) @@ -6185,7 +6418,9 @@ def diagnose_routing_path_network( max_exit_length=terminal_exit_max_length, ) exit_point = terminal_access_points[-1] if terminal_access_points else _terminal_exit_point(terminal, terminal_exit_length) - nearest_point, distance = nearest_point_on_network(network, exit_point) + nearest_payload = _nearest_point_on_network_payload(network, exit_point) + nearest_point = nearest_payload.get("point") if nearest_payload is not None else None + distance = nearest_payload.get("distance") if nearest_payload is not None else None access_carrier = _live_source_carrier(doc, terminal) access_live = access_carrier is not None and is_route_carrier(access_carrier) too_far = nearest_point is None or (max_distance > 0.0 and float(distance or 0.0) > max_distance) @@ -6197,11 +6432,17 @@ def diagnose_routing_path_network( "access_carrier": getattr(access_carrier, "Name", "") if access_carrier is not None else "", "nearest_network_distance_mm": None if distance is None else float(distance), "nearest_network_point": None if nearest_point is None else _point_payload(nearest_point), + "nearest_network_carrier_kind": "" if nearest_payload is None else nearest_payload.get("carrier_kind", ""), + "nearest_network_carrier_name": "" if nearest_payload is None else nearest_payload.get("carrier_name", ""), + "nearest_network_carrier_label": "" if nearest_payload is None else nearest_payload.get("carrier_label", ""), "terminal_access_max_distance_mm": float(max_distance), "terminal_exit_length_mm": float(max(float(terminal_exit_length or 0.0), 0.0)), + "terminal_exit_point": _point_payload(exit_point), "code": "terminal_access_missing" if not access_live else "terminal_access_too_far", } ) + # 未接入端子也保留端子出线折线,方便手测时判断是端子方向错、出线过短,还是主路径入口缺失。 + payload.update(_terminal_access_geometry_payload(terminal_access_points)) unconnected_terminals.append(payload) continue @@ -6219,6 +6460,23 @@ def diagnose_routing_path_network( "code": "terminal_access_long", } ) + if access_carrier is not None: + access_payload = _terminal_access_diagnostic_payload(access_carrier) + for key in ( + "target_kind", + "target_name", + "target_label", + "target_rule", + "target_distance_mm", + "nearest_main_path_kind", + "nearest_main_path_name", + "nearest_main_path_label", + "nearest_main_path_distance_mm", + "nearest_main_path_over_max_distance", + "endpoint_device_avoided", + ): + if key in access_payload: + payload[key] = access_payload.get(key) payload.update(_terminal_access_geometry_payload(access_points)) long_terminal_accesses.append(payload) @@ -6530,6 +6788,20 @@ def _diagnostic_terminal_text(sample): ) +def _diagnostic_nearest_network_carrier_text(sample): + if not isinstance(sample, dict): + return "" + label = str(sample.get("nearest_network_carrier_label", "") or "").strip() + name = str(sample.get("nearest_network_carrier_name", "") or "").strip() + kind = str(sample.get("nearest_network_carrier_kind", "") or "").strip() + text = label or name + if not text: + return "" + if kind: + return "{0}({1})".format(text, kind) + return text + + def _routing_path_network_diagnostic_message(diagnostic): if not isinstance(diagnostic, dict): return "布线路径网络检查失败:诊断结果无效。" @@ -6552,17 +6824,29 @@ def _routing_path_network_diagnostic_message(diagnostic): unconnected = _diagnostic_items(diagnostic.get("unconnected_terminals", []) or []) if unconnected: sample = unconnected[0] - message += "\n端子未接入:{0},距离最近网络 {1},当前端子接入最大距离 {2}。".format( + nearest_carrier = _diagnostic_nearest_network_carrier_text(sample) + nearest_carrier_clause = ",最近路径 {0}".format(nearest_carrier) if nearest_carrier else "" + message += "\n端子未接入:{0},距离最近网络 {1}{2},当前端子接入最大距离 {3}。".format( _diagnostic_terminal_text(sample), _diagnostic_distance_text(sample.get("nearest_network_distance_mm")), + nearest_carrier_clause, _diagnostic_distance_text(sample.get("terminal_access_max_distance_mm")), ) long_accesses = _diagnostic_items(diagnostic.get("long_terminal_accesses", []) or []) if long_accesses: sample = long_accesses[0] - message += "\n端子接入过长:{0},接入段 {1}。".format( + target_label = str(sample.get("target_label", "") or sample.get("target_name", "") or "").strip() + target_kind = str(sample.get("target_kind", "") or "").strip() + target_clause = "" + if target_label or target_kind: + target_text = target_label or "未知目标" + if target_kind: + target_text = "{0}({1})".format(target_text, target_kind) + target_clause = ",目标 {0}".format(target_text) + message += "\n端子接入过长:{0},接入段 {1}{2}。".format( _diagnostic_terminal_text(sample), _diagnostic_distance_text(sample.get("terminal_access_length_mm")), + target_clause, ) capped_exits = _diagnostic_items(diagnostic.get("capped_terminal_exits", []) or []) if capped_exits: @@ -6630,6 +6914,24 @@ def _routing_path_network_diagnostic_message(diagnostic): ) else: message += "\n线槽未接入端子主网络:{0}。".format(carrier_text) + fallback_targets = _diagnostic_items(diagnostic.get("terminal_access_fallback_targets", []) or []) + if fallback_targets: + sample = fallback_targets[0] + target_text = sample.get("target_label") or sample.get("target_name") or "布线面" + nearest_main = sample.get("nearest_main_path_label") or sample.get("nearest_main_path_name") or "" + if nearest_main: + message += "\n端子接入退回布线面:{0} 接入到 {1};最近主路径 {2},距离 {3},端子接入最大距离 {4}。".format( + _diagnostic_terminal_text(sample), + target_text, + nearest_main, + _diagnostic_distance_text(sample.get("nearest_main_path_distance_mm")), + _diagnostic_distance_text(sample.get("terminal_access_max_distance_mm")), + ) + else: + message += "\n端子接入退回布线面:{0} 接入到 {1},附近没有可用线槽/UserPath/过线孔主路径。".format( + _diagnostic_terminal_text(sample), + target_text, + ) isolated = _diagnostic_items(diagnostic.get("isolated_components", []) or []) if isolated: sample = isolated[0] diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index 6329853..3d1afa6 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -1,7 +1,8 @@ # FreeCADExchange terminal import helpers. -from collections import OrderedDict +from collections import Counter, OrderedDict import json +import uuid import FreeCAD as App @@ -40,7 +41,14 @@ def _normalize_terminal_entry(item, index): device_instance_id = (item.get("device_instance_id") or "").strip() element_uuid = (item.get("element_uuid") or "").strip() - terminal_display = (item.get("terminal_display") or "").strip() + terminal_display = ( + item.get("terminal_display") + or item.get("terminal_label") + or item.get("slot_name_hint") + or item.get("slot_name") + or "" + ).strip() + terminal_instance_id = (item.get("terminal_instance_id") or "").strip() slot_name_hint = ( item.get("slot_name_hint") or item.get("terminal_display") @@ -54,6 +62,7 @@ def _normalize_terminal_entry(item, index): "terminal_uuid": terminal_uuid, "device_instance_id": device_instance_id, "element_uuid": element_uuid, + "terminal_instance_id": terminal_instance_id, "terminal_display": terminal_display, "slot_name_hint": slot_name_hint, } @@ -152,21 +161,23 @@ def _device_embedded_terminal_entries(payload, existing_keys): # QET 的正式端子可能直接挂在 devices[].terminals[] 下。 # 直接调用本模块时也要读取它,避免正式布线匹配退回 local:* 端子。 - key = (element_uuid, terminal_uuid) - if key in seen: - continue - seen.add(key) terminal_display = ( terminal.get("terminal_display") or terminal.get("terminal_label") + or terminal.get("slot_name_hint") or terminal.get("slot_name") or "" ) + key = _terminal_context_key(element_uuid, terminal_uuid, terminal_display) + if key in seen: + continue + seen.add(key) entries.append( { "terminal_uuid": terminal_uuid, "element_uuid": element_uuid, "device_instance_id": device_instance_id, + "terminal_instance_id": (terminal.get("terminal_instance_id") or "").strip(), "terminal_display": terminal_display, "slot_name_hint": terminal_display, } @@ -192,20 +203,21 @@ def _wire_endpoint_terminal_entries(payload, existing_keys): if not terminal_uuid or not (element_uuid or device_instance_id): continue - key = (element_uuid, terminal_uuid) - if key in seen: - continue - seen.add(key) terminal_display = ( wire.get("{0}_terminal_display".format(side)) or wire.get("{0}_terminal_label".format(side)) or "" ) + key = _terminal_context_key(element_uuid, terminal_uuid, terminal_display) + if key in seen: + continue + seen.add(key) entries.append( { "terminal_uuid": terminal_uuid, "element_uuid": element_uuid, "device_instance_id": device_instance_id, + "terminal_instance_id": "", "terminal_display": terminal_display, "slot_name_hint": terminal_display, } @@ -275,6 +287,110 @@ def _terminal_existing_index(container): return index +def _terminal_existing_by_instance(container): + index = OrderedDict() + for obj in TerminalObjects.collect_terminal_objects(container): + terminal_instance_id = getattr(obj, "QetTerminalInstanceId", "").strip() + if terminal_instance_id and terminal_instance_id not in index: + index[terminal_instance_id] = obj + return index + + +def _terminal_existing_by_element_uuid(container): + index = OrderedDict() + for obj in TerminalObjects.collect_terminal_objects(container): + terminal_uuid = getattr(obj, "QetTerminalUuid", "").strip() + element_uuid = getattr(obj, "QetElementUuid", "").strip() + key = (element_uuid, terminal_uuid) + if element_uuid and terminal_uuid and key not in index: + index[key] = obj + return index + + +def _terminal_context_key(element_uuid, terminal_uuid, terminal_display): + return ( + (element_uuid or "").strip(), + (terminal_uuid or "").strip(), + (terminal_display or "").strip(), + ) + + +def _terminal_entry_context_key(entry): + return _terminal_context_key( + entry.get("element_uuid", ""), + entry.get("terminal_uuid", ""), + entry.get("terminal_display", ""), + ) + + +def _terminal_object_display(obj): + if "QetTerminalDisplay" in getattr(obj, "PropertiesList", []): + return getattr(obj, "QetTerminalDisplay", "").strip() + return ( + getattr(obj, "Label", "").strip() + or getattr(obj, "QetTemplateSlotName", "").strip() + ) + + +def _terminal_existing_by_context(container): + index = OrderedDict() + for obj in TerminalObjects.collect_terminal_objects(container): + terminal_uuid = getattr(obj, "QetTerminalUuid", "").strip() + element_uuid = getattr(obj, "QetElementUuid", "").strip() + terminal_display = _terminal_object_display(obj) + key = _terminal_context_key(element_uuid, terminal_uuid, terminal_display) + if element_uuid and terminal_uuid and terminal_display and key not in index: + index[key] = obj + return index + + +def _terminal_instance_id_seed(project_uuid, entry): + values = [ + project_uuid, + entry.get("device_instance_id", ""), + entry.get("element_uuid", ""), + entry.get("terminal_uuid", ""), + entry.get("terminal_display", ""), + entry.get("slot_name_hint", ""), + ] + return "qet-freecad-terminal|" + "|".join(str(value or "").strip() for value in values) + + +def _stable_terminal_instance_id(project_uuid, entry): + return str(uuid.uuid5(uuid.NAMESPACE_URL, _terminal_instance_id_seed(project_uuid, entry))) + + +def _terminal_instance_counts(items): + values = [] + for item in list(items or []): + if not isinstance(item, dict): + continue + value = str(item.get("terminal_instance_id", "") or "").strip() + if value: + values.append(value) + return Counter(values) + + +def _repair_terminal_instance_id(entry, project_uuid, raw_counts, used_ids): + raw_value = str(entry.get("terminal_instance_id", "") or "").strip() + must_repair = ( + not raw_value + or raw_counts.get(raw_value, 0) > 1 + or raw_value in used_ids + ) + if must_repair: + # QET 当前 v2 快照里可能出现重复 terminal_uuid / terminal_instance_id。 + # FreeCAD 侧短期修复必须避开导出顺序,所以用设备实例、2D 元件、端子 UUID 和脚号生成稳定 ID。 + raw_value = _stable_terminal_instance_id(project_uuid, entry) + suffix = 1 + while raw_value in used_ids: + raw_value = str(uuid.uuid5(uuid.NAMESPACE_URL, _terminal_instance_id_seed(project_uuid, entry) + "|{0}".format(suffix))) + suffix += 1 + entry["terminal_instance_id"] = raw_value + used_ids.add(raw_value) + return must_repair + + def _terminal_existing_local_by_slot(container): index = {} for obj in TerminalObjects.collect_terminal_objects(container): @@ -428,6 +544,8 @@ def _create_terminal_object(doc, terminal_uuid, entry, slot, terminal_group, pro instance_id, label=terminal_label, slot_name=slot.get("name", ""), + terminal_instance_id=entry.get("terminal_instance_id", ""), + terminal_display=entry.get("terminal_display", ""), ) _set_terminal_geometry_source(terminal_obj, slot.get("source", "template")) _set_terminal_local_route_points(terminal_obj, slot) @@ -435,6 +553,45 @@ def _create_terminal_object(doc, terminal_uuid, entry, slot, terminal_group, pro return terminal_obj +def _remove_terminal_from_group(doc, terminal_group, terminal_obj): + try: + children = list(getattr(terminal_group, "Group", []) or []) + if terminal_obj in children: + children.remove(terminal_obj) + terminal_group.Group = children + except Exception: + try: + terminal_group.Group.remove(terminal_obj) + except Exception: + pass + try: + if doc.getObject(terminal_obj.Name) is not None: + doc.removeObject(terminal_obj.Name) + except Exception: + pass + + +def _terminal_object_context_key(terminal_obj): + return _terminal_context_key( + getattr(terminal_obj, "QetElementUuid", ""), + getattr(terminal_obj, "QetTerminalUuid", ""), + _terminal_object_display(terminal_obj), + ) + + +def _remove_stale_qet_terminals(doc, terminal_group, expected_contexts, used_objects): + removed = 0 + for terminal_obj in list(TerminalObjects.collect_terminal_objects(terminal_group)): + terminal_uuid = getattr(terminal_obj, "QetTerminalUuid", "").strip() + if TerminalObjects.is_local_terminal_uuid(terminal_uuid): + continue + # 当前 2d_to_3d.json 是端子快照;不在快照中的历史 QET 端子会干扰回写和布线匹配,需要清理。 + if terminal_obj not in used_objects or _terminal_object_context_key(terminal_obj) not in expected_contexts: + _remove_terminal_from_group(doc, terminal_group, terminal_obj) + removed += 1 + return removed + + def import_terminals_from_payload(payload, scene_path=""): _append_debug_log("TerminalImport.import_terminals_from_payload entered") @@ -456,17 +613,25 @@ def import_terminals_from_payload(payload, scene_path=""): continue element_uuid = (item.get("element_uuid") or "").strip() terminal_uuid = (item.get("terminal_uuid") or "").strip() + terminal_display = ( + item.get("terminal_display") + or item.get("terminal_label") + or item.get("slot_name_hint") + or item.get("slot_name") + or "" + ).strip() if element_uuid and terminal_uuid: - terminal_entry_keys.add((element_uuid, terminal_uuid)) + terminal_entry_keys.add(_terminal_context_key(element_uuid, terminal_uuid, terminal_display)) embedded_entries = _device_embedded_terminal_entries(payload, terminal_entry_keys) terminal_entries.extend(embedded_entries) terminal_entry_keys.update( - (entry["element_uuid"], entry["terminal_uuid"]) + _terminal_entry_context_key(entry) for entry in embedded_entries if entry.get("element_uuid") and entry.get("terminal_uuid") ) synthesized_entries = _wire_endpoint_terminal_entries(payload, terminal_entry_keys) terminal_entries.extend(synthesized_entries) + raw_terminal_instance_counts = _terminal_instance_counts(terminal_entries) device_lookup = _payload_device_lookup(payload) @@ -487,10 +652,12 @@ def import_terminals_from_payload(payload, scene_path=""): "skipped_missing_device": 0, "skipped_invalid_entry": 0, "skipped_unmatched_parent": 0, + "repaired_terminal_instance_ids": 0, "warnings": [], } grouped = OrderedDict() + used_terminal_instance_ids = set() for index, item in enumerate(terminal_entries): report["total_terminals"] += 1 try: @@ -503,6 +670,13 @@ def import_terminals_from_payload(payload, scene_path=""): if not _terminal_belongs_to_payload_devices(entry, device_lookup): report["skipped_unmatched_parent"] += 1 continue + if _repair_terminal_instance_id( + entry, + project_uuid, + raw_terminal_instance_counts, + used_terminal_instance_ids, + ): + report["repaired_terminal_instance_ids"] += 1 device_group = _locate_device_group(doc, entry) if device_group is None: @@ -537,7 +711,12 @@ def import_terminals_from_payload(payload, scene_path=""): terminal_group = _terminal_container_for_device(doc, device_group, project_uuid) existing_by_uuid = _terminal_existing_index(terminal_group) + existing_by_instance = _terminal_existing_by_instance(terminal_group) + existing_by_context = _terminal_existing_by_context(terminal_group) + existing_by_element_uuid = _terminal_existing_by_element_uuid(terminal_group) existing_local_by_slot = _terminal_existing_local_by_slot(terminal_group) + entry_uuid_counts = Counter(entry.get("terminal_uuid", "") for entry in entries) + expected_contexts = set(_terminal_entry_context_key(entry) for entry in entries) used_uuids = set() used_objects = set() used_slot_names = set() @@ -587,18 +766,40 @@ def import_terminals_from_payload(payload, scene_path=""): if slot_name: used_slot_names.add(slot_name) - terminal_obj = existing_by_uuid.get(terminal_uuid) + terminal_obj = existing_by_instance.get(entry.get("terminal_instance_id", "")) + if terminal_obj in used_objects: + terminal_obj = None + if terminal_obj is None: + terminal_obj = existing_by_context.get(_terminal_entry_context_key(entry)) + if terminal_obj in used_objects: + terminal_obj = None + if terminal_obj is None: + terminal_obj = existing_by_element_uuid.get((entry.get("element_uuid", ""), terminal_uuid)) + if terminal_obj in used_objects: + terminal_obj = None + if terminal_obj is None: + terminal_obj = ( + existing_by_uuid.get(terminal_uuid) + if entry_uuid_counts.get(terminal_uuid, 0) == 1 + else None + ) + if terminal_obj in used_objects: + terminal_obj = None if terminal_obj is None: terminal_obj = existing_local_by_slot.get(slot_name) + if terminal_obj in used_objects: + terminal_obj = None if terminal_obj is not None: TerminalObjects.set_terminal_semantics( terminal_obj, project_uuid, - device_element_uuid, + entry.get("element_uuid", "") or device_element_uuid, terminal_uuid, device_instance_id, label=_terminal_entry_label(entry, slot, terminal_uuid), slot_name=slot.get("name", ""), + terminal_instance_id=entry.get("terminal_instance_id", ""), + terminal_display=entry.get("terminal_display", ""), ) _set_terminal_geometry_source(terminal_obj, slot.get("source", "template")) _set_terminal_local_route_points(terminal_obj, slot) @@ -616,7 +817,7 @@ def import_terminals_from_payload(payload, scene_path=""): slot, terminal_group, project_uuid, - device_element_uuid, + entry.get("element_uuid", "") or device_element_uuid, device_instance_id, ) report["imported_terminals"] += 1 @@ -624,12 +825,14 @@ def import_terminals_from_payload(payload, scene_path=""): TerminalObjects.set_terminal_semantics( terminal_obj, project_uuid, - device_element_uuid, + entry.get("element_uuid", "") or device_element_uuid, terminal_uuid, device_instance_id, - label=_terminal_entry_label(entry, slot, terminal_uuid), - slot_name=slot.get("name", ""), - ) + label=_terminal_entry_label(entry, slot, terminal_uuid), + slot_name=slot.get("name", ""), + terminal_instance_id=entry.get("terminal_instance_id", ""), + terminal_display=entry.get("terminal_display", ""), + ) _set_terminal_geometry_source(terminal_obj, slot.get("source", "template")) _set_terminal_local_route_points(terminal_obj, slot) try: @@ -649,6 +852,13 @@ def import_terminals_from_payload(payload, scene_path=""): _hide_object(source_obj) report["reused_template_hints"] += 1 + if entries: + report["removed_terminals"] += _remove_stale_qet_terminals( + doc, + terminal_group, + expected_contexts, + used_objects, + ) TerminalObjects.sort_group_children(terminal_group) TerminalObjects.sort_group_children(root_group) diff --git a/src/Mod/FreeCADExchange/TerminalObjects.py b/src/Mod/FreeCADExchange/TerminalObjects.py index e4cc32f..b5729ba 100644 --- a/src/Mod/FreeCADExchange/TerminalObjects.py +++ b/src/Mod/FreeCADExchange/TerminalObjects.py @@ -548,6 +548,8 @@ def set_terminal_semantics( instance_id, label="", slot_name="", + terminal_instance_id="", + terminal_display="", ): ensure_string_property( obj, @@ -589,6 +591,27 @@ def set_terminal_semantics( "Parent instance UUID for this terminal", instance_id, ) + # QetInstanceId 表示父设备实例;QetTerminalInstanceId 才是 3D 端子对象自己的稳定实例 ID。 + # 旧工程可能没有该字段,缺省时先兼容退回父设备实例,导入器会尽量写入真正端子实例 ID。 + terminal_instance_id = ( + (terminal_instance_id or "").strip() + or getattr(obj, "QetTerminalInstanceId", "").strip() + or instance_id + ) + ensure_string_property( + obj, + "QetTerminalInstanceId", + "QET Exchange", + "Stable 3D terminal instance UUID", + terminal_instance_id, + ) + ensure_string_property( + obj, + "QetTerminalDisplay", + "QET Exchange", + "QET terminal display text / terminal number", + (terminal_display or "").strip(), + ) ensure_string_property( obj, "Role", diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 395f615..300b77f 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -541,6 +541,43 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("Dashed", wire.ViewObject.DrawStyle) self.assertEqual("DashLine", style["line_type"]) + def test_eplan_connection_route_records_applied_visual_style_metadata(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-style-metadata", + options={ + "wire_style_id": "style-blue", + "wire_style_lookup": lambda style_id, project_uuid: { + "id": style_id, + "line_color": "#3366CC", + "line_width": 2.5, + "line_type": "虚线", + }, + }, + ) + + wire = result["wire"] + self.assertTrue(wire.QetWireStyleApplied) + self.assertEqual("#3366CC", wire.QetAppliedWireLineColor) + self.assertEqual("2.5", wire.QetAppliedWireLineWidth) + self.assertEqual("Dashed", wire.QetAppliedWireDrawStyle) + def test_eplan_connection_route_accepts_bare_hex_color_and_diameter_width(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -2744,6 +2781,69 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(fallback_carrier.Name, bridges[0].QetRouteBridgeLeftSourceName) self.assertEqual(main_path.Name, bridges[0].QetRouteBridgeRightSourceName) + def test_controller_terminal_access_fallback_bridge_prefers_diagnostic_nearest_main_path(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + fallback_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面", + ) + nearer_but_not_recommended = routing_network.create_route_carrier( + doc, + [app.Vector(120, 0, 0), app.Vector(220, 0, 0)], + project_uuid="project-1", + kind="UserPath", + label="近处非推荐路径", + ) + recommended_main_path = routing_network.create_route_carrier( + doc, + [app.Vector(300, 30, 0), app.Vector(400, 30, 0)], + project_uuid="project-1", + kind="UserPath", + label="诊断推荐主路径", + ) + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") + diagnostic.QetDiagnosticKind = "RoutingPathNetwork" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "terminal_access_fallback_targets": [ + { + "target_kind": "RoutingRange", + "target_name": fallback_carrier.Name, + "target_label": "安装板布线面", + "nearest_main_path_name": recommended_main_path.Name, + "nearest_main_path_label": "诊断推荐主路径", + "terminal_uuid": "terminal-ud8-as", + } + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + + result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_terminal_access_fallback_targets() + + bridges = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "TerminalAccessFallbackBridge" + ] + self.assertEqual(1, result["terminal_access_fallback_user_path_bridges"]) + self.assertEqual(1, len(bridges)) + self.assertEqual(fallback_carrier.Name, bridges[0].QetRouteBridgeLeftSourceName) + self.assertEqual(recommended_main_path.Name, bridges[0].QetRouteBridgeRightSourceName) + self.assertNotEqual(nearer_but_not_recommended.Name, bridges[0].QetRouteBridgeRightSourceName) + def test_controller_terminal_access_fallback_bridge_prefers_nearest_segment_not_endpoint(self): _install_fake_freecad() terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -2890,6 +2990,49 @@ class AutoRoutingTest(unittest.TestCase): ) self.assertIn("自动诊断桥接:生成 UserPath 1 条", message) + def test_route_eplan_connections_keeps_unconnected_terminal_bridge_summary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + original_create = routing_network.create_user_path_bridges_from_diagnostic_suggestions + + def fake_create(_doc, _diagnostic, project_uuid=""): + return { + "suggestions": 1, + "created": [object()], + "duplicates": 0, + "stale_suggestions": 0, + "unconnected_terminal_access_bridge_targets": 1, + "unconnected_terminal_access_user_path_bridges": 1, + "unconnected_terminal_access_bridge_duplicates": 0, + "unconnected_terminal_access_bridge_pair_labels": ["A1 接入段 -> 最近线槽"], + } + + try: + routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create + report = auto_routing.route_eplan_connections( + doc, + payload={"project_uuid": "project-1", "wires": []}, + options={"auto_create_diagnostic_bridges": True}, + project_uuid="project-1", + update_network=False, + ) + finally: + routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create + + summary = report["auto_diagnostic_bridges"] + message = auto_routing.format_eplan_connection_route_report(report) + self.assertEqual(1, summary["created_count"]) + self.assertEqual(1, summary["unconnected_terminal_access_bridge_targets"]) + self.assertEqual(1, summary["unconnected_terminal_access_user_path_bridges"]) + self.assertEqual(0, summary["unconnected_terminal_access_bridge_duplicates"]) + self.assertEqual(["A1 接入段 -> 最近线槽"], summary["unconnected_terminal_access_bridge_pair_labels"]) + self.assertIn("未接入端子接入段 1 个", message) + self.assertIn("配对:A1 接入段 -> 最近线槽", message) + def test_route_eplan_connections_does_not_auto_create_diagnostic_bridge_when_disabled(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -2948,7 +3091,17 @@ class AutoRoutingTest(unittest.TestCase): def fake_create(_doc, diagnostic, project_uuid=""): calls["create"] += 1 if int(diagnostic.get("pass_index", 0) or 0) <= 2: - return {"suggestions": 1, "created": [object()], "duplicates": 0, "stale_suggestions": 0} + pair_label = "A{0} 接入段 -> 最近线槽".format(calls["create"]) + return { + "suggestions": 1, + "created": [object()], + "duplicates": 0, + "stale_suggestions": 0, + "unconnected_terminal_access_bridge_targets": 1, + "unconnected_terminal_access_user_path_bridges": 1, + "unconnected_terminal_access_bridge_duplicates": 0, + "unconnected_terminal_access_bridge_pair_labels": [pair_label], + } return {"suggestions": 0, "created": [], "duplicates": 0, "stale_suggestions": 0} try: @@ -2963,6 +3116,13 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(2, result["user_path_bridges"]) self.assertEqual(2, result["diagnostic_suggestions"]) self.assertEqual(3, result["diagnostic_passes"]) + self.assertEqual(2, result["unconnected_terminal_access_bridge_targets"]) + self.assertEqual(2, result["unconnected_terminal_access_user_path_bridges"]) + self.assertEqual(0, result["unconnected_terminal_access_bridge_duplicates"]) + self.assertEqual( + ["A1 接入段 -> 最近线槽", "A2 接入段 -> 最近线槽"], + result["unconnected_terminal_access_bridge_pair_labels"], + ) self.assertEqual(3, calls["check"]) self.assertEqual(3, calls["create"]) @@ -4856,7 +5016,7 @@ class AutoRoutingTest(unittest.TestCase): def test_controller_selects_terminal_access_fallback_terminal_and_device_from_samples(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] @@ -4921,7 +5081,7 @@ class AutoRoutingTest(unittest.TestCase): def test_controller_selects_terminal_access_fallback_targets_from_path_network_diagnostic(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] @@ -5003,7 +5163,7 @@ class AutoRoutingTest(unittest.TestCase): def test_controller_selects_terminal_access_endpoint_device_avoidance_from_path_diagnostic(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] @@ -5083,7 +5243,7 @@ class AutoRoutingTest(unittest.TestCase): def test_controller_selects_unconnected_terminal_access_issues_from_path_diagnostic(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] @@ -5101,6 +5261,20 @@ class AutoRoutingTest(unittest.TestCase): app.Vector(0, 0, 0), ) device.addObject(terminal) + nearest_path = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + label="最近线槽", + ) + access_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(0, 0, 20)], + project_uuid="project-1", + kind="TerminalAccess", + label="A1 接入段", + ) diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") diagnostic.QetDiagnosticKind = "RoutingPathNetwork" diagnostic.QetProjectUuid = "project-1" @@ -5113,7 +5287,10 @@ class AutoRoutingTest(unittest.TestCase): "terminal_uuid": "terminal-unconnected", "parent_device_name": device.Name, "parent_device_label": "未接入设备", + "access_carrier": access_carrier.Name, "nearest_network_distance_mm": 125.0, + "nearest_network_carrier_name": nearest_path.Name, + "nearest_network_carrier_label": "最近线槽", "terminal_access_max_distance_mm": 50.0, }, { @@ -5136,18 +5313,22 @@ class AutoRoutingTest(unittest.TestCase): result = auto_routing_panel.AutoRoutingController().select_unconnected_terminal_access_issues() - self.assertEqual(2, result["selected_unconnected_terminal_access_objects"]) + self.assertEqual(4, result["selected_unconnected_terminal_access_objects"]) self.assertEqual(1, result["selected_unconnected_terminal_access_terminals"]) self.assertEqual(1, result["selected_unconnected_terminal_access_devices"]) + self.assertEqual(1, result["selected_unconnected_terminal_access_carriers"]) + self.assertEqual(1, result["selected_unconnected_terminal_access_nearest_paths"]) self.assertEqual([terminal.Name], result["selected_unconnected_terminal_access_terminal_names"]) self.assertEqual([device.Name], result["selected_unconnected_terminal_access_device_names"]) + self.assertEqual([access_carrier.Name], result["selected_unconnected_terminal_access_carrier_names"]) + self.assertEqual([nearest_path.Name], result["selected_unconnected_terminal_access_nearest_path_names"]) self.assertEqual(["MissingUnconnectedTerminal", "MissingUnconnectedDevice"], result["missing_unconnected_terminal_access_refs"]) self.assertEqual(125.0, result["max_unconnected_terminal_access_distance_mm"]) - self.assertEqual([terminal, device], selected) + self.assertEqual([terminal, device, access_carrier, nearest_path], selected) def test_controller_selects_terminal_exit_issue_terminals_from_path_diagnostic(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] @@ -5460,7 +5641,7 @@ class AutoRoutingTest(unittest.TestCase): def test_controller_selects_long_terminal_accesses_from_latest_batch_diagnostic(self): _install_fake_freecad() - terminal_objects, wiring_objects, _routing_network, _auto_routing = _reload_modules() + terminal_objects, wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") app = sys.modules["FreeCAD"] gui = sys.modules["FreeCADGui"] @@ -5469,6 +5650,20 @@ class AutoRoutingTest(unittest.TestCase): terminal_objects.ensure_root_group(doc, "project-1") terminal_a = _terminal(doc, terminal_objects, "Terminal325", "terminal-325", app.Vector(0, 0, 0)) terminal_b = _terminal(doc, terminal_objects, "Terminal326", "terminal-326", app.Vector(10, 0, 0)) + access_a = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="TerminalAccess", + label="325 接入段", + ) + target_a = routing_network.create_route_carrier( + doc, + [app.Vector(100, 0, 20), app.Vector(200, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + label="远处线槽", + ) diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" @@ -5477,7 +5672,14 @@ class AutoRoutingTest(unittest.TestCase): { "routing_path_network_diagnostic": { "long_terminal_accesses": [ - {"terminal_uuid": "terminal-325", "name": "Terminal325", "label": "325"}, + { + "terminal_uuid": "terminal-325", + "name": "Terminal325", + "label": "325", + "access_carrier": access_a.Name, + "target_name": target_a.Name, + "target_label": "远处线槽", + }, {"terminal_uuid": "terminal-326", "name": "Terminal326", "label": "326"}, {"terminal_uuid": "terminal-404", "name": "Terminal404", "label": "404"}, ] @@ -5498,8 +5700,12 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(2, result["selected_long_terminal_accesses"]) self.assertEqual(["Terminal325", "Terminal326"], result["selected_long_terminal_names"]) + self.assertEqual(1, result["selected_long_terminal_access_carriers"]) + self.assertEqual([access_a.Name], result["selected_long_terminal_access_carrier_names"]) + self.assertEqual(1, result["selected_long_terminal_access_targets"]) + self.assertEqual([target_a.Name], result["selected_long_terminal_access_target_names"]) self.assertEqual(["terminal-404"], result["missing_long_terminal_refs"]) - self.assertEqual([terminal_a, terminal_b], selected) + self.assertEqual([terminal_a, access_a, target_a, terminal_b], selected) def test_controller_selects_long_terminal_accesses_from_path_network_diagnostic(self): _install_fake_freecad() @@ -7105,6 +7311,59 @@ class AutoRoutingTest(unittest.TestCase): end_point = created[0].Points[-1] self.assertEqual((50.0, 5.0, 20.0), (end_point.x, end_point.y, end_point.z)) + def test_terminal_access_fallback_diagnostic_records_nearest_rejected_main_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="安装板布线面", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 150, 20), app.Vector(100, 150, 20)], + project_uuid="project-1", + kind="WireDuct", + label="上方线槽", + ) + + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=100.0, + ) + result = auto_routing.check_eplan_routing_path_network( + doc, + project_uuid="project-1", + options={"terminal_access_max_distance": 100.0}, + ) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + + self.assertEqual(1, len(created)) + self.assertEqual("RoutingRange", created[0].QetTerminalAccessTargetKind) + self.assertEqual("fallback_only", created[0].QetTerminalAccessTargetRule) + self.assertEqual("WireDuct", created[0].QetTerminalAccessNearestMainPathKind) + self.assertEqual("上方线槽", created[0].QetTerminalAccessNearestMainPathLabel) + self.assertEqual(150.0, created[0].QetTerminalAccessNearestMainPathDistanceMm) + self.assertEqual(100.0, created[0].QetTerminalAccessMaxDistanceMm) + self.assertTrue(result["diagnostic"]["terminal_access_fallback_targets"]) + sample = payload["terminal_access_fallback_targets"][0] + self.assertEqual("WireDuct", sample["nearest_main_path_kind"]) + self.assertEqual("上方线槽", sample["nearest_main_path_label"]) + self.assertEqual(150.0, sample["nearest_main_path_distance_mm"]) + self.assertEqual(100.0, sample["terminal_access_max_distance_mm"]) + self.assertTrue(sample["nearest_main_path_over_max_distance"]) + self.assertIn("端子接入退回布线面", diagnostic_group.Group[0].QetDiagnosticMessage) + self.assertIn("最近主路径 上方线槽", diagnostic_group.Group[0].QetDiagnosticMessage) + def test_terminal_access_prefers_main_path_over_routing_range_in_same_component(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -7371,6 +7630,7 @@ class AutoRoutingTest(unittest.TestCase): [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", + label="远处线槽", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") @@ -7387,12 +7647,23 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticMessage) self.assertIn("unconnected_terminals", payload["issue_codes"]) self.assertEqual(1, len(payload["unconnected_terminals"])) - self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"]) - self.assertEqual(1000.0, payload["unconnected_terminals"][0]["terminal_access_max_distance_mm"]) + unconnected = payload["unconnected_terminals"][0] + self.assertEqual("terminal-far", unconnected["terminal_uuid"]) + self.assertEqual(1000.0, unconnected["terminal_access_max_distance_mm"]) + self.assertEqual({"x": 5000.0, "y": 0.0, "z": 20.0}, unconnected["terminal_exit_point"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 20.0}, unconnected["nearest_network_point"]) + self.assertEqual("WireDuct", unconnected["nearest_network_carrier_kind"]) + self.assertEqual("远处线槽", unconnected["nearest_network_carrier_label"]) + self.assertEqual(2, len(unconnected["terminal_access_points"])) + self.assertEqual({"x": 5000.0, "y": 0.0, "z": 0.0}, unconnected["terminal_access_points"][0]) + self.assertEqual({"x": 5000.0, "y": 0.0, "z": 20.0}, unconnected["terminal_access_points"][1]) + self.assertEqual("z", unconnected["terminal_access_dominant_axis"]) + self.assertEqual(20.0, unconnected["terminal_access_axis_lengths_mm"]["z"]) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertIn("端子未接入", message) self.assertIn("terminal-far", message) self.assertIn("4900.0 mm", message) + self.assertIn("最近路径 远处线槽(WireDuct)", message) self.assertIn("端子接入最大距离 1000.0 mm", message) self.assertIn("补一段线槽/辅助路径", message) @@ -7413,6 +7684,7 @@ class AutoRoutingTest(unittest.TestCase): [app.Vector(1000, 0, 20), app.Vector(1100, 0, 20)], project_uuid="project-1", kind="WireDuct", + label="远处线槽", ) routing_network.create_terminal_access_carriers_from_document( doc, @@ -7437,6 +7709,9 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(900.0, long_access["terminal_access_length_mm"]) self.assertEqual("PEN", long_access["parent_device_label"]) self.assertEqual("DevicePEN", long_access["parent_device_name"]) + self.assertEqual("WireDuct", long_access["target_kind"]) + self.assertEqual("远处线槽", long_access["target_label"]) + self.assertEqual(900.0, long_access["target_distance_mm"]) self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, long_access["terminal_origin"]) self.assertEqual("x", long_access["terminal_access_dominant_axis"]) self.assertEqual(2, len(long_access["terminal_access_points"])) @@ -7446,6 +7721,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("TerminalLongAccess", message) self.assertIn("terminal-long-access", message) self.assertIn("900.0 mm", message) + self.assertIn("目标 远处线槽(WireDuct)", message) def test_check_routing_path_network_ignores_isolated_routing_range_only_components(self): _install_fake_freecad() @@ -7631,6 +7907,11 @@ class AutoRoutingTest(unittest.TestCase): self.assertIsNotNone(access_carrier) self.assertEqual("TerminalAccess", access_carrier.QetRouteCarrierKind) self.assertEqual("UserPath", sample["target_kind"]) + self.assertTrue(sample["endpoint_device_avoided"]) + self.assertEqual( + {"xmin": -10.0, "xmax": 10.0, "ymin": -10.0, "ymax": 10.0, "zmin": -10.0, "zmax": 10.0}, + sample["endpoint_device_bbox"], + ) self.assertGreater(sample["access_length_mm"], 0.0) self.assertGreaterEqual(len(sample["access_points"]), 2) @@ -7683,12 +7964,20 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("terminal-corrected", corrected_sample["terminal_uuid"]) self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, corrected_sample["exit_direction"]) self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, corrected_sample["original_exit_direction"]) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, corrected_sample["origin"]) + self.assertEqual({"x": 20.0, "y": 0.0, "z": 0.0}, corrected_sample["exit_point"]) + self.assertFalse(corrected_sample["local_route_used"]) + self.assertEqual(0, corrected_sample["local_route_point_count"]) self.assertEqual(1, len(payload["capped_terminal_exits"])) capped_sample = payload["capped_terminal_exits"][0] self.assertEqual("TerminalCappedExit", capped_sample["name"]) self.assertEqual("terminal-capped", capped_sample["terminal_uuid"]) self.assertEqual(30.0, capped_sample["max_exit_length_mm"]) self.assertEqual(30.0, capped_sample["actual_exit_length_mm"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, capped_sample["origin"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 30.0}, capped_sample["exit_point"]) + self.assertFalse(capped_sample["local_route_used"]) + self.assertEqual(0, capped_sample["local_route_point_count"]) self.assertTrue(capped_sample["exit_length_capped"]) def test_compact_routing_path_network_diagnostic_keeps_terminal_access_quality_samples(self): @@ -7699,6 +7988,36 @@ class AutoRoutingTest(unittest.TestCase): "issues": [ {"severity": "warning", "code": "terminal_access_fallback_targets", "count": 1}, {"severity": "info", "code": "terminal_access_endpoint_device_avoidance", "count": 1}, + {"severity": "warning", "code": "terminal_exit_direction_corrected", "count": 1}, + {"severity": "warning", "code": "terminal_exit_length_capped", "count": 1}, + {"severity": "warning", "code": "invalid_terminal_local_routes", "count": 1}, + ], + "corrected_terminal_exits": [ + { + "name": "TerminalCorrectedExit", + "terminal_uuid": "terminal-corrected", + "parent_device_name": "DeviceQF2", + "exit_direction": {"x": 1.0, "y": 0.0, "z": 0.0}, + "original_exit_direction": {"x": 0.0, "y": 0.0, "z": 1.0}, + "origin": {"x": 0.0, "y": 0.0, "z": 0.0}, + "exit_point": {"x": 20.0, "y": 0.0, "z": 0.0}, + "local_route_used": False, + "local_route_point_count": 0, + } + ], + "capped_terminal_exits": [ + { + "name": "TerminalCappedExit", + "terminal_uuid": "terminal-capped", + "parent_device_name": "DeviceQF3", + "actual_exit_length_mm": 30.0, + "max_exit_length_mm": 30.0, + "origin": {"x": 100.0, "y": 0.0, "z": 0.0}, + "exit_point": {"x": 100.0, "y": 0.0, "z": 30.0}, + "local_route_used": False, + "local_route_point_count": 0, + "exit_length_capped": True, + } ], "terminal_access_fallback_targets": [ { @@ -7732,6 +8051,33 @@ class AutoRoutingTest(unittest.TestCase): "target_label": "左侧主路径", "target_rule": "main_path_nearest", "target_distance_mm": 40.0, + "endpoint_device_avoided": True, + "endpoint_device_bbox": { + "xmin": -10.0, + "xmax": 10.0, + "ymin": -10.0, + "ymax": 10.0, + "zmin": -10.0, + "zmax": 10.0, + }, + } + ], + "invalid_terminal_local_routes": [ + { + "name": "TerminalInvalidLocalRoute", + "terminal_uuid": "terminal-invalid-local", + "parent_device_name": "DeviceInvalidLocal", + "property_name": "QetTerminalLocalRoutePointsJson", + "reason": "local_route_end_inside_device_bbox", + "local_route_end_point": {"x": 5.0, "y": 0.0, "z": 0.0}, + "endpoint_device_bbox": { + "xmin": -10.0, + "xmax": 10.0, + "ymin": -10.0, + "ymax": 10.0, + "zmin": -10.0, + "zmax": 10.0, + }, } ], } @@ -7755,6 +8101,26 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("TerminalAccessAvoid001", avoidance_sample["access_carrier_name"]) self.assertEqual("A1 接入段", avoidance_sample["access_carrier_label"]) self.assertEqual("UserPath001", avoidance_sample["target_name"]) + self.assertTrue(avoidance_sample["endpoint_device_avoided"]) + self.assertEqual(-10.0, avoidance_sample["endpoint_device_bbox"]["xmin"]) + self.assertEqual(1, len(payload["corrected_terminal_exits"])) + corrected_exit_sample = payload["corrected_terminal_exits"][0] + self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, corrected_exit_sample["origin"]) + self.assertEqual({"x": 20.0, "y": 0.0, "z": 0.0}, corrected_exit_sample["exit_point"]) + self.assertFalse(corrected_exit_sample["local_route_used"]) + self.assertEqual(0, corrected_exit_sample["local_route_point_count"]) + self.assertEqual(1, len(payload["capped_terminal_exits"])) + capped_exit_sample = payload["capped_terminal_exits"][0] + self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, capped_exit_sample["origin"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 30.0}, capped_exit_sample["exit_point"]) + self.assertFalse(capped_exit_sample["local_route_used"]) + self.assertEqual(0, capped_exit_sample["local_route_point_count"]) + self.assertEqual(1, len(payload["invalid_terminal_local_routes"])) + invalid_local_sample = payload["invalid_terminal_local_routes"][0] + self.assertEqual("terminal-invalid-local", invalid_local_sample["terminal_uuid"]) + self.assertEqual("local_route_end_inside_device_bbox", invalid_local_sample["reason"]) + self.assertEqual({"x": 5.0, "y": 0.0, "z": 0.0}, invalid_local_sample["local_route_end_point"]) + self.assertEqual(-10.0, invalid_local_sample["endpoint_device_bbox"]["xmin"]) def test_zero_distance_user_path_endpoint_splits_wire_duct_segment(self): _install_fake_freecad() @@ -7889,6 +8255,58 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual([], duplicated) + def test_diagnostic_bridge_connects_unconnected_terminal_access_to_nearest_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + access_carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(0, 0, 20)], + project_uuid="project-1", + kind="TerminalAccess", + label="A1 接入段", + ) + nearest_path = routing_network.create_route_carrier( + doc, + [app.Vector(100, 0, 20), app.Vector(200, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + label="最近线槽", + ) + + report = routing_network.create_user_path_bridges_from_diagnostic_suggestions( + doc, + { + "unconnected_terminals": [ + { + "access_carrier": access_carrier.Name, + "nearest_network_carrier_name": nearest_path.Name, + "nearest_network_carrier_label": "最近线槽", + "terminal_uuid": "terminal-a1", + } + ], + }, + project_uuid="project-1", + ) + + self.assertEqual(1, report["suggestions"]) + self.assertEqual(1, report["unconnected_terminal_access_bridge_targets"]) + self.assertEqual(1, report["unconnected_terminal_access_user_path_bridges"]) + self.assertEqual(0, report["unconnected_terminal_access_bridge_duplicates"]) + self.assertEqual(["A1 接入段 -> 最近线槽"], report["unconnected_terminal_access_bridge_pair_labels"]) + self.assertEqual(1, len(report["created"])) + bridge = report["created"][0] + self.assertEqual("UserPath", bridge.QetRouteCarrierKind) + self.assertEqual("UnconnectedTerminalAccessBridge", bridge.QetRouteBridgeKind) + self.assertEqual(access_carrier.Name, bridge.QetRouteBridgeLeftSourceName) + self.assertEqual(nearest_path.Name, bridge.QetRouteBridgeRightSourceName) + self.assertEqual([(0.0, 0.0, 20.0), (100.0, 0.0, 20.0)], [ + (point.x, point.y, point.z) + for point in bridge.Points + ]) + def test_check_routing_path_network_warns_for_invalid_terminal_exit_direction(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -7976,6 +8394,48 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("端子局部路径无效", message) self.assertIn("terminal-invalid-local-path", message) + def test_check_routing_path_network_warns_when_terminal_local_route_ends_inside_device(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceLocalRouteInside") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalLocalRouteInside", "terminal-local-inside", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [5, 0, 0]]) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "DeviceLocalRouteInsideBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 10)) + device.addObject(body) + routing_network.create_route_carrier( + doc, + [app.Vector(100, 0, 0), app.Vector(200, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) + message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + + self.assertFalse(result["ok"]) + self.assertIn("invalid_terminal_local_routes", payload["issue_codes"]) + self.assertEqual(1, len(payload["invalid_terminal_local_routes"])) + sample = payload["invalid_terminal_local_routes"][0] + self.assertEqual("terminal-local-inside", sample["terminal_uuid"]) + self.assertEqual("local_route_end_inside_device_bbox", sample["reason"]) + self.assertEqual({"x": 5.0, "y": 0.0, "z": 0.0}, sample["local_route_end_point"]) + self.assertEqual( + {"xmin": -10.0, "xmax": 10.0, "ymin": -10.0, "ymax": 10.0, "zmin": -10.0, "zmax": 10.0}, + sample["endpoint_device_bbox"], + ) + self.assertIn("端子局部路径无效", message) + self.assertIn("local_route_end_inside_device_bbox", message) + def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -8027,17 +8487,56 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("布线路径网络检查发现", message) self.assertIn("首个问题:external_issue", message) - def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): + def test_format_routing_path_network_report_calls_out_terminal_exit_diagnostics(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="线槽A", - project_uuid="project-1", + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + diagnostic = { + "ok": False, + "issues": [ + {"severity": "warning", "code": "terminal_exit_direction_corrected", "count": 1}, + {"severity": "warning", "code": "terminal_exit_length_capped", "count": 1}, + ], + "corrected_terminal_exits": [ + { + "name": "TerminalCorrectedExit", + "terminal_uuid": "terminal-corrected", + "original_exit_direction": {"x": 0.0, "y": 0.0, "z": 1.0}, + "exit_direction": {"x": 1.0, "y": 0.0, "z": 0.0}, + } + ], + "capped_terminal_exits": [ + { + "name": "TerminalCappedExit", + "terminal_uuid": "terminal-capped", + "actual_exit_length_mm": 30.0, + "max_exit_length_mm": 30.0, + "device_exit_required_length_mm": 510.0, + } + ], + } + + message = auto_routing.format_routing_path_network_report(diagnostic) + + self.assertIn("端子默认出线方向已校正", message) + self.assertIn("TerminalCorrectedExit", message) + self.assertIn("原方向 (0.0, 0.0, 1.0)", message) + self.assertIn("采用方向 (1.0, 0.0, 0.0)", message) + self.assertIn("端子出线长度截断", message) + self.assertIn("TerminalCappedExit", message) + self.assertIn("实际 30.0 mm / 上限 30.0 mm", message) + self.assertIn("设备出线需求 510.0 mm", message) + + def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="线槽A", + project_uuid="project-1", kind="WireDuct", ) @@ -8314,6 +8813,38 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, len(diagnostic_group.Group)) self.assertEqual("RoutingPreflight", diagnostic_group.Group[0].QetDiagnosticKind) + def test_auto_routing_controller_readiness_prepares_path_network_before_preflight(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing_panel.AutoRoutingController().check_routing_readiness() + + self.assertNotIn("no_route_network", report["issue_codes"]) + self.assertNotIn("routing_sources_not_generated", report["issue_codes"]) + self.assertGreater(report["route_network_segments"], 0) + self.assertEqual(1, report["prepared_layout"]["wire_duct_carriers"]) + self.assertEqual(2, report["prepared_layout"]["terminal_access_carriers"]) + def test_route_eplan_connections_prepares_layout_space_like_eplan_route(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() @@ -10101,6 +10632,114 @@ class AutoRoutingTest(unittest.TestCase): self.assertIsNotNone(result) self.assertLessEqual(calls["shortest_path"], 16) + def test_route_eplan_connections_reuses_entry_candidates_for_same_batch_endpoint(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-{0}".format(index), + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + for index in range(6) + ], + } + calls = {"start_endpoint": 0} + original_connection_point_candidates = routing_network.connection_point_candidates + start_point = start.Placement.Base + + def counted_connection_point_candidates(network, point, *args, **kwargs): + if auto_routing._distance(point, start_point) <= 0.001: + calls["start_endpoint"] += 1 + return original_connection_point_candidates(network, point, *args, **kwargs) + + routing_network.connection_point_candidates = counted_connection_point_candidates + try: + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={ + "terminal_exit_length": 0.0, + "network_entry_candidate_limit": 2, + "batch_network_entry_candidate_limit": 2, + }, + ) + finally: + routing_network.connection_point_candidates = original_connection_point_candidates + + self.assertEqual(6, report["routed"]) + self.assertLessEqual(calls["start_endpoint"], 1) + + def test_route_eplan_connections_reuses_route_track_for_lane_adjustment(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 10, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 10, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 50), app.Vector(100, 5, 50)], + label="Shared Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + calls = {"shortest_path": 0} + original_shortest_path = routing_network.shortest_path_with_carriers + + def counted_shortest_path(*args, **kwargs): + calls["shortest_path"] += 1 + return original_shortest_path(*args, **kwargs) + + routing_network.shortest_path_with_carriers = counted_shortest_path + try: + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={ + "terminal_exit_length": 0.0, + "network_entry_candidate_limit": 1, + "batch_network_entry_candidate_limit": 1, + }, + ) + finally: + routing_network.shortest_path_with_carriers = original_shortest_path + + self.assertEqual(2, report["routed"]) + self.assertEqual(2, calls["shortest_path"]) + self.assertEqual([0, 1], [route["lane"]["index"] for route in report["routes"]]) + def test_eplan_connection_route_marks_clearance_warning_against_expanded_obstacle_bbox(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -10386,6 +11025,37 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual({"x": 1.0, "y": 0.0, "z": 0.0}, diagnostics["exit_direction"]) self.assertEqual({"x": 15.0, "y": 35.0, "z": 0.0}, diagnostics["exit_point"]) + def test_terminal_access_ignores_local_route_that_ends_inside_endpoint_device_bbox(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDeviceInvalidLocalRoute") + root.addObject(device) + terminal = _terminal(doc, terminal_objects, "TerminalInvalidLocalRouteInside", "terminal-invalid-local-route-inside", app.Vector(0, 0, 0)) + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") + terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [5, 0, 0]]) + device.addObject(terminal) + body = doc.addObject("Part::Feature", "InvalidLocalRouteDeviceBody") + body.Shape = FakeShape(FakeBoundBox(-10, 10, -10, 10, -10, 10)) + device.addObject(body) + + points = routing_network.terminal_access_path_points( + terminal, + exit_length=20.0, + max_exit_length=80.0, + ) + diagnostics = routing_network.terminal_access_diagnostics( + terminal, + exit_length=20.0, + max_exit_length=80.0, + ) + + self.assertEqual([(0.0, 0.0, 0.0), (0.0, 0.0, 20.0)], [(point.x, point.y, point.z) for point in points]) + self.assertFalse(diagnostics["local_route_used"]) + self.assertEqual("default_exit", diagnostics["exit_rule"]) + def test_terminal_access_prefers_main_path_over_nearer_routing_range_and_records_rule(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -10535,6 +11205,11 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("TerminalWithLocalExit", sample["terminal_name"]) self.assertEqual("QETDeviceAccessBox", sample["parent_device_name"]) self.assertEqual("UserPath", sample["target_kind"]) + self.assertTrue(sample["endpoint_device_avoided"]) + self.assertEqual( + {"xmin": -10.0, "xmax": 10.0, "ymin": -10.0, "ymax": 10.0, "zmin": -10.0, "zmax": 10.0}, + sample["endpoint_device_bbox"], + ) self.assertGreater(sample["access_length_mm"], 0.0) self.assertGreaterEqual(len(sample["access_points"]), 2) self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, sample["access_points"][0]) @@ -11049,6 +11724,136 @@ class AutoRoutingTest(unittest.TestCase): sample["end_missing_endpoint_reason_code"], ) + def test_preflight_reports_duplicate_payload_terminal_instance_ids(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + payload = { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-a", + "terminals": [ + { + "terminal_uuid": "terminal-a", + "terminal_instance_id": "shared-terminal-instance", + "element_uuid": "element-a", + "terminal_display": "1", + } + ], + }, + { + "device_instance_id": "device-b", + "terminals": [ + { + "terminal_uuid": "terminal-b", + "terminal_instance_id": "shared-terminal-instance", + "element_uuid": "element-b", + "terminal_display": "1", + } + ], + }, + ], + "wires": [], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + + self.assertEqual(1, report["duplicate_payload_terminal_instance_id_count"]) + self.assertEqual( + { + "terminal_instance_id": "shared-terminal-instance", + "count": 2, + }, + report["duplicate_payload_terminal_instance_id_samples"][0], + ) + self.assertIn("duplicate_payload_terminal_instance_ids", report["issue_codes"]) + + def test_preflight_reports_duplicate_3d_terminal_uuids(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalA", "shared-terminal", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalB", "shared-terminal", app.Vector(100, 0, 0)) + payload = { + "project_uuid": "project-1", + "wires": [], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + compact = auto_routing._compact_routing_preflight_report(report) + + self.assertEqual(1, report["duplicate_terminal_uuid_count"]) + self.assertIn("duplicate_3d_terminal_uuids", report["issue_codes"]) + self.assertIn("3D工程端子 UUID 重复", message) + self.assertEqual(1, compact["duplicate_terminal_uuid_count"]) + self.assertIn("3D端子UUID重复", compact["issue_labels"]) + + def test_preflight_reports_payload_terminals_not_referenced_by_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-sa", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-target", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + ) + + payload = { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-id27", + "display_tag": "ID:27", + "terminals": [ + { + "terminal_uuid": "terminal-as", + "element_uuid": "element-id27", + "terminal_display": "as", + }, + { + "terminal_uuid": "terminal-sa", + "element_uuid": "element-id27", + "terminal_display": "sa", + }, + ], + } + ], + "wires": [ + { + "wire_id": "wire-id27-sa", + "wire_label": "N401", + "start_element_uuid": "element-id27", + "start_terminal_uuid": "terminal-sa", + "start_terminal_display": "sa", + "end_element_uuid": "element-target", + "end_terminal_uuid": "terminal-target", + "end_terminal_display": "1", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + + self.assertEqual(1, report["unreferenced_payload_terminal_count"]) + self.assertEqual("ID:27", report["unreferenced_payload_terminal_samples"][0]["device_label"]) + self.assertEqual("as", report["unreferenced_payload_terminal_samples"][0]["terminal_display"]) + self.assertIn("payload_terminals_without_wires", report["issue_codes"]) + self.assertIn("未被 wires[] 引用的端子", message) + compact = auto_routing._compact_routing_preflight_report(report) + self.assertEqual(1, compact["unreferenced_payload_terminal_count"]) + self.assertEqual("ID:27", compact["unreferenced_payload_terminal_samples"][0]["device_label"]) + self.assertIn("输入端子未被导线引用", compact["issue_labels"]) + def test_route_eplan_connections_from_payload_reports_missing_device_binding_metadata(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -11438,7 +12243,16 @@ class AutoRoutingTest(unittest.TestCase): kind="WireDuct", label="诊断桥接后主路径", ) - return {"suggestions": 1, "created": [carrier], "duplicates": 0, "stale_suggestions": 0} + return { + "suggestions": 1, + "created": [carrier], + "duplicates": 0, + "stale_suggestions": 0, + "unconnected_terminal_access_bridge_targets": 1, + "unconnected_terminal_access_user_path_bridges": 1, + "unconnected_terminal_access_bridge_duplicates": 0, + "unconnected_terminal_access_bridge_pair_labels": ["A1 接入段 -> 最近线槽"], + } routing_network.diagnose_routing_path_network = fake_diagnostic routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create @@ -11449,6 +12263,9 @@ class AutoRoutingTest(unittest.TestCase): routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create self.assertEqual(1, report["auto_diagnostic_bridges"]["created_count"]) + self.assertEqual(1, report["auto_diagnostic_bridges"]["unconnected_terminal_access_bridge_targets"]) + self.assertEqual(1, report["auto_diagnostic_bridges"]["unconnected_terminal_access_user_path_bridges"]) + self.assertEqual(["A1 接入段 -> 最近线槽"], report["auto_diagnostic_bridges"]["unconnected_terminal_access_bridge_pair_labels"]) self.assertEqual({"main_path_routes": 1, "fallback_routes": 0}, report["route_path_usage"]) self.assertEqual(["Routed"], list(report["route_status_counts"].keys())) self.assertNotIn("main_path_not_used", report["issue_codes"]) @@ -12630,15 +13447,33 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) - def test_route_eplan_connections_classifies_disconnected_network_as_missing_route_network(self): + def test_collect_obstacles_skips_geometry_inside_route_carrier_group(self): _install_fake_freecad() - terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( + carriers = doc.addObject("App::DocumentObjectGroup", "QETWiring_02_Carriers") + carrier_body = doc.addObject("Part::Feature", "WireDuctSolid") + carrier_body.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + carriers.addObject(carrier_body) + obstacle = doc.addObject("Part::Feature", "DeviceObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) + + obstacles = auto_routing.collect_obstacles(doc) + cache = auto_routing._obstacle_candidate_cache(doc) + cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) + + self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) + self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) + + def test_route_eplan_connections_classifies_disconnected_network_as_missing_route_network(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], project_uuid="project-1", @@ -14018,6 +14853,9 @@ class AutoRoutingTest(unittest.TestCase): { "name": "TerminalUnconnected", "terminal_uuid": "terminal-unconnected", + "access_carrier": "QETRouteCarrier_TerminalAccess001", + "nearest_network_carrier_name": "QETRouteCarrier_WireDuct001", + "nearest_network_carrier_label": "最近线槽", "nearest_network_distance_mm": 125.0, } ], @@ -14029,6 +14867,7 @@ class AutoRoutingTest(unittest.TestCase): actions = auto_routing._routing_diagnostic_recommended_actions(summary) self.assertIn("点击“选择未接入端子”定位未接入路由网络或接入距离超限的端子", actions) + self.assertIn("点击“按诊断建议生成桥接”尝试自动补未接入端子接入段到最近路径的 UserPath 桥", actions) self.assertIn("补端子附近 UserPath/线槽入口,或确认设备装配位置和端子接入最大距离", actions) def test_routing_diagnostic_recommended_actions_include_wire_outside_boundary_selection(self): @@ -14086,6 +14925,39 @@ class AutoRoutingTest(unittest.TestCase): payload["route_path_usage"], ) + def test_compact_batch_report_includes_main_path_target_bridge_summary(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 1, + "routed": 1, + "auto_main_path_target_bridges": { + "enabled": True, + "pairs": 1, + "created_count": 1, + "duplicates": 0, + "created_pair_labels": ["Left duct -> Right duct"], + "wire_uuids": ["wire-main-target-bridge"], + "rerouted": True, + }, + "routes": [], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual( + { + "enabled": True, + "pairs": 1, + "created_count": 1, + "duplicates": 0, + "created_pair_labels": ["Left duct -> Right duct"], + "wire_uuids": ["wire-main-target-bridge"], + "rerouted": True, + }, + payload["auto_main_path_target_bridges"], + ) + def test_compact_batch_report_summarizes_terminal_access_consumption(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -14643,7 +15515,12 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, report["routed"]) self.assertEqual(1, report["hidden_route_carriers"]) + self.assertEqual(1, report["visible_routed_wires"]) self.assertFalse(carrier.ViewObject.Visibility) + routed_group = doc.getObject("QETWiring_04_Routed") + routed_wire = list(getattr(routed_group, "Group", []) or [])[0] + self.assertTrue(routed_group.ViewObject.Visibility) + self.assertTrue(routed_wire.ViewObject.Visibility) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") diagnostics = [ item @@ -14654,6 +15531,85 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, diagnostic_payload["hidden_route_carriers"]) self.assertTrue(diagnostic_payload["routing_path_network_updated"] is False) + def test_route_report_includes_first_stage_visual_closeout_summary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "style-green", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=False, + options={ + "wire_style_lookup": lambda style_id, project_uuid: { + "id": style_id, + "line_color": "#00AA00", + "line_width": 1.5, + "line_type": "Solid", + }, + }, + ) + + self.assertEqual(1, report["routed_wire_visibility"]["routed"]) + self.assertEqual(1, report["routed_wire_visibility"]["visible"]) + self.assertEqual(0, report["routed_wire_visibility"]["hidden"]) + self.assertEqual(1, report["wire_style_application"]["expected"]) + self.assertEqual(1, report["wire_style_application"]["applied"]) + self.assertEqual(0, report["wire_style_application"]["missing_application"]) + self.assertEqual(1, report["route_carrier_visibility"]["total"]) + self.assertEqual(0, report["route_carrier_visibility"]["visible_after_hide"]) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + diagnostics = [ + item + for item in list(getattr(diagnostic_group, "Group", []) or []) + if getattr(item, "QetDiagnosticKind", "") == "RoutingConnectionBatch" + ] + diagnostic_payload = json.loads(diagnostics[0].QetDiagnosticJson) + self.assertEqual(1, diagnostic_payload["routed_wire_visibility"]["visible"]) + self.assertEqual(1, diagnostic_payload["wire_style_application"]["applied"]) + self.assertEqual(0, diagnostic_payload["route_carrier_visibility"]["visible_after_hide"]) + + def test_route_report_explains_styled_black_wires(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 2, + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "wire_style_application": { + "expected": 2, + "applied": 2, + "missing_application": 0, + "styled_black": 1, + }, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("黑色导线:1 条来自 wire_properties 样式", message) + def test_route_eplan_connections_ignores_global_payload_from_other_project(self): _install_fake_freecad() terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() @@ -15188,6 +16144,35 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("已隐藏走线路径辅助对象:3 条。", message) + def test_route_issue_codes_flag_visual_closeout_regressions(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 1, + "routed": 1, + "routed_wire_visibility": { + "routed": 1, + "visible": 0, + "hidden": 1, + }, + "wire_style_application": { + "expected": 1, + "applied": 0, + "missing_application": 1, + }, + "route_carrier_visibility": { + "expected_hidden": True, + "total": 2, + "visible_after_hide": 1, + }, + } + + issue_codes = auto_routing._routing_connection_batch_issue_codes(report) + + self.assertIn("routed_wires_not_visible", issue_codes) + self.assertIn("wire_styles_not_applied", issue_codes) + self.assertIn("route_carriers_still_visible", issue_codes) + def test_route_report_warns_when_routes_use_surface_or_auxiliary_paths(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -15230,6 +16215,7 @@ class AutoRoutingTest(unittest.TestCase): "routed": 3, "collision_warnings": 0, "skipped_missing_terminal": 0, + "show_candidate_debug_warnings": True, "routes": [ { "lane": {"index": 2, "spacing_mm": 10.0}, @@ -15254,6 +16240,7 @@ class AutoRoutingTest(unittest.TestCase): "routed": 3, "collision_warnings": 0, "skipped_missing_terminal": 0, + "show_candidate_debug_warnings": True, "routes": [ { "wire_label": "N-CROWDED", @@ -15279,6 +16266,7 @@ class AutoRoutingTest(unittest.TestCase): "routed": 3, "collision_warnings": 0, "skipped_missing_terminal": 0, + "show_candidate_debug_warnings": True, "routes": [ { "wire_label": "N-CROWDED", @@ -15304,6 +16292,7 @@ class AutoRoutingTest(unittest.TestCase): "routed": 3, "collision_warnings": 0, "skipped_missing_terminal": 0, + "show_candidate_debug_warnings": True, "routes": [ { "wire_label": "N-CROWDED", @@ -15360,6 +16349,7 @@ class AutoRoutingTest(unittest.TestCase): "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, + "show_candidate_debug_warnings": True, "routes": [ { "wire_label": "N1", @@ -15440,6 +16430,7 @@ class AutoRoutingTest(unittest.TestCase): "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, + "show_candidate_debug_warnings": True, "routes": [ { "wire_label": "N1", @@ -15796,6 +16787,7 @@ class AutoRoutingTest(unittest.TestCase): [app.Vector(1000, 0, 20), app.Vector(1100, 0, 20)], project_uuid="project-1", kind="WireDuct", + label="远处线槽", ) routing_network.create_terminal_access_carriers_from_document( doc, @@ -15832,6 +16824,9 @@ class AutoRoutingTest(unittest.TestCase): sample = diagnostic["long_terminal_accesses"][0] self.assertEqual("terminal-start", sample["terminal_uuid"]) self.assertEqual("PEN", sample["parent_device_label"]) + self.assertEqual("WireDuct", sample["target_kind"]) + self.assertEqual("远处线槽", sample["target_label"]) + self.assertEqual(900.0, sample["target_distance_mm"]) self.assertEqual("x", sample["terminal_access_dominant_axis"]) self.assertEqual(2, len(sample["terminal_access_points"])) @@ -16365,114 +17360,392 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(50.0, wire.Points[0].y) self.assertEqual(50.0, wire.Points[-1].y) - def test_route_eplan_connections_lane_index_accounts_for_existing_routed_segments(self): + def test_route_eplan_connections_disambiguates_duplicate_terminal_uuid_by_terminal_display(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( + root = terminal_objects.ensure_root_group(doc, "project-1") + + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 50, 0)) + terminal_objects.set_terminal_semantics( + start, + "project-1", + "device-start", + "terminal-start", + "instance-start", + label="S1", + ) + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_shared") + root.addObject(device) + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-end") + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-end") + terminal_group = terminal_objects.ensure_terminal_group( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + device, project_uuid="project-1", - kind="WireDuct", + instance_id="instance-end", ) - auto_routing.route_eplan_connection_between_terminals( + for name, label, point in ( + ("QETTerminal_shared_1", "1", app.Vector(120, 50, 0)), + ("QETTerminal_shared_339", "339", app.Vector(120, 0, 0)), + ): + terminal = terminal_objects.create_lcs_object( + doc, + name, + placement=app.Placement(point, app.Rotation()), + label=label, + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "device-end", + "shared-terminal", + "instance-end", + label=label, + slot_name=label, + ) + routing_network.create_route_carrier( doc, - start, - end, - wire_uuid="existing-wire", - options={"lane_spacing": 10.0, "lane_axis": "y"}, + [app.Vector(0, 50, 20), app.Vector(120, 50, 20)], + project_uuid="project-1", + kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { - "wire_id": "new-wire", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, + "wire_id": "wire-display-match", + "start_element_uuid": "device-start", + "start_instance_id": "instance-start", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "S1", + "end_element_uuid": "device-end", + "end_instance_id": "instance-end", + "end_terminal_uuid": "shared-terminal", + "end_terminal_display": "1", + } ], } - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"lane_spacing": 10.0, "lane_axis": "y"}, - ) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) - self.assertEqual(1, report["routes"][0]["lane"]["index"]) + self.assertEqual(1, report["routed"]) + self.assertEqual(0, report["skipped_missing_terminal"]) routed_group = doc.getObject("QETWiring_04_Routed") - new_wire = [ - wire - for wire in list(getattr(routed_group, "Group", []) or []) - if getattr(wire, "QetWireUuid", "") == "new-wire" - ][0] - self.assertEqual("1", new_wire.QetRouteLaneIndex) - self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in new_wire.Points[1:-1])) + wire = list(getattr(routed_group, "Group", []) or [])[0] + self.assertEqual("1", wire.QetEndTerminalDisplay) + self.assertEqual(50.0, wire.Points[-1].y) - def test_route_eplan_connections_auto_lane_axis_offsets_perpendicular_to_shared_segment(self): + def test_route_eplan_connections_repairs_duplicate_terminal_metadata_from_v2_device_terminals(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + root = terminal_objects.ensure_root_group(doc, "project-1") + + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 50, 0)) + terminal_objects.set_terminal_semantics( + start, + "project-1", + "device-start", + "terminal-start", + "instance-start", + label="S1", + ) + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_shared") + root.addObject(device) + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-shared") + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-shared") + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="instance-shared", + ) + for name, label, point in ( + ("QETTerminal_shared_a", "A-old", app.Vector(120, 0, 0)), + ("QETTerminal_shared_b", "B-old", app.Vector(120, 50, 0)), + ): + terminal = terminal_objects.create_lcs_object( + doc, + name, + placement=app.Placement(point, app.Rotation()), + label=label, + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "wrong-element", + "shared-terminal", + "instance-shared", + label=label, + slot_name=label, + ) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + [app.Vector(0, 50, 20), app.Vector(120, 50, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", - "wires": [ + "devices": [ { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, + "device_instance_id": "instance-shared", + "display_tag": "T1", + "terminals": [ + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-a", + "terminal_instance_id": "instance-shared", + "terminal_display": "A1", + }, + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-b", + "terminal_instance_id": "instance-shared", + "terminal_display": "B1", + }, + ], + } + ], + "wires": [ { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, + "wire_id": "wire-repaired", + "start_element_uuid": "device-start", + "start_instance_id": "instance-start", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "S1", + "end_element_uuid": "element-b", + "end_instance_id": "instance-shared", + "end_terminal_uuid": "shared-terminal", + "end_terminal_display": "B1", + } ], } - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"lane_spacing": 10.0}, - ) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) - self.assertEqual(1, report["routes"][1]["lane"]["index"]) - self.assertEqual("x", report["routes"][1]["lane"]["axis"]) + self.assertEqual(1, report["routed"]) + self.assertEqual(0, report["skipped_missing_terminal"]) routed_group = doc.getObject("QETWiring_04_Routed") - second_wire = [ - wire - for wire in list(getattr(routed_group, "Group", []) or []) - if getattr(wire, "QetWireUuid", "") == "wire-b" - ][0] - self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) - self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + wire = list(getattr(routed_group, "Group", []) or [])[0] + self.assertEqual("B1", wire.QetEndTerminalDisplay) + self.assertEqual("element-b", wire.QetEndElementUuid) + self.assertEqual(50.0, wire.Points[-1].y) - def test_route_eplan_connections_auto_lane_axis_avoids_cabinet_boundary(self): + def test_preflight_routeability_uses_endpoint_context_for_duplicate_terminal_uuid(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") - boundary.Label = "柜内空间" - boundary.Shape = FakeShape(FakeBoundBox(-10, 110, 0, 100, -10, 80)) - routing_network.mark_cabinet_interior_boundaries_from_selection( - [FakeSelectionItem(obj=boundary)] + + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 50, 0)) + terminal_objects.set_terminal_semantics( + start, + "project-1", + "device-start", + "terminal-start", + "instance-start", + label="S1", + terminal_display="S1", + ) + end_a = _terminal(doc, terminal_objects, "TerminalEndA", "shared-terminal", app.Vector(120, 0, 0)) + terminal_objects.set_terminal_semantics( + end_a, + "project-1", + "element-a", + "shared-terminal", + "instance-end", + label="A1", + terminal_display="A1", + ) + end_b = _terminal(doc, terminal_objects, "TerminalEndB", "shared-terminal", app.Vector(120, 50, 0)) + terminal_objects.set_terminal_semantics( + end_b, + "project-1", + "element-b", + "shared-terminal", + "instance-end", + label="B1", + terminal_display="B1", + ) + + selected_end_labels = [] + original_build_network_route = auto_routing.build_network_route + + def fake_build_network_route(start_terminal, end_terminal, **_kwargs): + selected_end_labels.append(getattr(end_terminal, "Label", "")) + if getattr(end_terminal, "Label", "") == "B1": + return {"points": [start_terminal.Placement.Base, end_terminal.Placement.Base]} + return None + + auto_routing.build_network_route = fake_build_network_route + try: + summary = auto_routing._preflight_routeability_summary( + doc, + [ + { + "wire_id": "wire-display-match", + "start_element_uuid": "device-start", + "start_instance_id": "instance-start", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "S1", + "end_element_uuid": "element-b", + "end_instance_id": "instance-end", + "end_terminal_uuid": "shared-terminal", + "end_terminal_display": "B1", + } + ], + auto_routing.index_terminals(doc), + options={ + "preflight_routeability_sample_limit": 1, + "__terminal_candidates": auto_routing._collect_routable_terminals(doc), + }, + ) + finally: + auto_routing.build_network_route = original_build_network_route + + self.assertEqual(["B1"], selected_end_labels) + self.assertEqual(1, summary["eligible_wires"]) + self.assertEqual(0, summary["unrouteable_wires"]) + + def test_preflight_reports_available_terminal_objects_when_terminal_uuid_repeats(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalSharedA", "shared-terminal", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalSharedB", "shared-terminal", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + report = auto_routing.preflight_eplan_connections( + doc, + {"project_uuid": "project-1", "wires": []}, + ) + + self.assertEqual(2, report["available_terminals"]) + self.assertEqual(2, report["available_terminal_objects"]) + self.assertEqual(1, report["unique_terminal_uuids"]) + + def test_route_eplan_connections_lane_index_accounts_for_existing_routed_segments(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="existing-wire", + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "new-wire", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + + self.assertEqual(1, report["routes"][0]["lane"]["index"]) + routed_group = doc.getObject("QETWiring_04_Routed") + new_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "new-wire" + ][0] + self.assertEqual("1", new_wire.QetRouteLaneIndex) + self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in new_wire.Points[1:-1])) + + def test_route_eplan_connections_auto_lane_axis_offsets_perpendicular_to_shared_segment(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0}, + ) + + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + self.assertEqual("x", report["routes"][1]["lane"]["axis"]) + routed_group = doc.getObject("QETWiring_04_Routed") + second_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "wire-b" + ][0] + self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + + def test_route_eplan_connections_auto_lane_axis_avoids_cabinet_boundary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, 0, 100, -10, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] ) _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 95, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 95, 0)) @@ -16637,78 +17910,1104 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("Alternate Duct", second_labels) self.assertNotIn("Direct Duct", second_labels) - def test_route_eplan_connections_respects_route_segment_capacity_before_detouring(self): + def test_main_path_target_bridge_helper_links_targets_when_route_falls_back_to_routing_range(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartC", "terminal-start-c", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndC", "terminal-end-c", app.Vector(100, 0, 0)) - direct = routing_network.create_route_carrier( + left = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="Direct Duct", + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + label="Left duct", project_uuid="project-1", kind="WireDuct", ) - direct.QetRouteCarrierCapacity = 2 - routing_network.create_route_carrier( + right = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], - label="Left Bridge", + [app.Vector(100, 0, 20), app.Vector(100, 100, 20)], + label="Right duct", project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( + fallback = routing_network.create_route_carrier( doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="Alternate Duct", + [app.Vector(0, 50, 20), app.Vector(100, 50, 20)], + label="Panel fallback", + project_uuid="project-1", + kind="RoutingRange", + ) + report = { + "routes": [ + { + "wire_uuid": "wire-main-target-bridge", + "network": { + "start_terminal_access_target_kind": "WireDuct", + "start_terminal_access_target_name": left.Name, + "start_terminal_access_target_label": left.Label, + "end_terminal_access_target_kind": "WireDuct", + "end_terminal_access_target_name": right.Name, + "end_terminal_access_target_label": right.Label, + }, + "route_track": { + "segments": [ + { + "carrier": { + "kind": "RoutingRange", + "name": fallback.Name, + "label": fallback.Label, + } + } + ] + }, + } + ] + } + + bridge_report = auto_routing._create_main_path_target_bridges_from_report( + doc, + report, + project_uuid="project-1", + ) + + self.assertEqual(1, bridge_report["pairs"]) + self.assertEqual(1, bridge_report["created_count"]) + self.assertEqual(["wire-main-target-bridge"], bridge_report["wire_uuids"]) + self.assertTrue( + any(getattr(obj, "QetRouteBridgeKind", "") == "MainPathTargetBridge" for obj in doc.Objects) + ) + + def test_main_path_target_bridge_detours_around_device_obstacle(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + lower = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(20, 0, 0)], + label="Lower duct", project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( + upper = routing_network.create_route_carrier( doc, - [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], - label="Right Bridge", + [app.Vector(0, 0, 100), app.Vector(20, 0, 100)], + label="Upper duct", + project_uuid="project-1", + kind="WireDuct", + ) + obstacle = doc.addObject("Part::Feature", "MiddleDevice") + obstacle.Label = "Middle device" + obstacle.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, 40, 60)) + report = { + "routes": [ + { + "wire_uuid": "wire-obstacle-bridge", + "network": { + "start_terminal_access_target_kind": "WireDuct", + "start_terminal_access_target_name": lower.Name, + "start_terminal_access_target_label": lower.Label, + "end_terminal_access_target_kind": "WireDuct", + "end_terminal_access_target_name": upper.Name, + "end_terminal_access_target_label": upper.Label, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "Panel range"}} + ] + }, + } + ] + } + + bridge_report = auto_routing._create_main_path_target_bridges_from_report( + doc, + report, + project_uuid="project-1", + ) + + self.assertEqual(1, bridge_report["created_count"]) + bridge = [ + obj + for obj in doc.Objects + if getattr(obj, "QetRouteBridgeKind", "") == "MainPathTargetBridge" + ][0] + bridge_points = list(getattr(bridge, "Points", []) or []) + self.assertGreater(len(bridge_points), 2) + self.assertEqual( + [], + auto_routing.detect_collisions( + bridge_points, + auto_routing.collect_obstacles(doc, options={"obstacle_clearance": 0.0}), + ), + ) + + def test_main_path_target_bridge_ignores_unbound_structural_cabinet_frame(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + left = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + label="Left duct", + project_uuid="project-1", + kind="WireDuct", + ) + right = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], + label="Right duct", project_uuid="project-1", kind="WireDuct", ) + cabinet = doc.addObject("Part::Feature", "CabinetFrame") + cabinet.Label = "NAU03_Cabinet_Frame" + cabinet.Shape = FakeShape(FakeBoundBox(-200, 200, -200, 200, -10, 110)) + + bridges = auto_routing._create_user_path_bridge_between_objects_avoiding_obstacles( + doc, + left, + right, + project_uuid="project-1", + bridge_kind="MainPathTargetBridge", + ) + + self.assertEqual(1, len(bridges)) + bridge_points = list(getattr(bridges[0], "Points", []) or []) + self.assertEqual((0.0, 0.0, 0.0), (bridge_points[0].x, bridge_points[0].y, bridge_points[0].z)) + self.assertEqual((0.0, 0.0, 100.0), (bridge_points[-1].x, bridge_points[-1].y, bridge_points[-1].z)) + bridge_obstacles = auto_routing._route_bridge_obstacles(doc, left, right, left, right) + self.assertNotIn("NAU03_Cabinet_Frame", [item.get("label") for item in bridge_obstacles]) + + def test_main_path_target_bridge_helper_retries_all_wires_for_same_target_pair(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + left = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + label="Left duct", + project_uuid="project-1", + kind="WireDuct", + ) + right = routing_network.create_route_carrier( + doc, + [app.Vector(100, 0, 20), app.Vector(100, 100, 20)], + label="Right duct", + project_uuid="project-1", + kind="WireDuct", + ) + fallback = routing_network.create_route_carrier( + doc, + [app.Vector(0, 50, 20), app.Vector(100, 50, 20)], + label="Panel fallback", + project_uuid="project-1", + kind="RoutingRange", + ) + + def route(wire_uuid): + return { + "wire_uuid": wire_uuid, + "network": { + "start_terminal_access_target_kind": "WireDuct", + "start_terminal_access_target_name": left.Name, + "start_terminal_access_target_label": left.Label, + "end_terminal_access_target_kind": "WireDuct", + "end_terminal_access_target_name": right.Name, + "end_terminal_access_target_label": right.Label, + }, + "route_track": { + "segments": [ + { + "carrier": { + "kind": "RoutingRange", + "name": fallback.Name, + "label": fallback.Label, + } + } + ] + }, + } + + bridge_report = auto_routing._create_main_path_target_bridges_from_report( + doc, + {"routes": [route("wire-a"), route("wire-b")]}, + project_uuid="project-1", + ) + + self.assertEqual(1, bridge_report["pairs"]) + self.assertEqual(1, bridge_report["created_count"]) + self.assertEqual(["wire-a", "wire-b"], bridge_report["wire_uuids"]) + + def test_main_path_target_retry_payload_requires_targets_and_forbids_fallback(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + payload = { + "project_uuid": "project-1", + "wires": [ + {"wire_id": "wire-a", "start_terminal_uuid": "s-a", "end_terminal_uuid": "e-a"}, + {"wire_id": "wire-b", "start_terminal_uuid": "s-b", "end_terminal_uuid": "e-b"}, + ], + } + report = { + "routes": [ + { + "wire_uuid": "wire-a", + "network": { + "start_terminal_access_target_kind": "WireDuct", + "start_terminal_access_target_name": "QETRouteCarrier_Main", + "start_terminal_access_target_label": "Main duct", + "end_terminal_access_target_kind": "WireDuct", + "end_terminal_access_target_name": "QETRouteCarrier_Main", + "end_terminal_access_target_label": "Main duct", + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "Panel range"}} + ] + }, + }, + { + "wire_uuid": "wire-b", + "network": { + "start_terminal_access_target_kind": "WireDuctOpenEnd", + "start_terminal_access_target_name": "QETRouteCarrier_OpenEnd", + "start_terminal_access_target_label": "Main duct open end", + "end_terminal_access_target_kind": "WireDuct", + "end_terminal_access_target_name": "QETRouteCarrier_Main", + "end_terminal_access_target_label": "Main duct", + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "Panel range"}} + ] + }, + }, + ] + } + + retry_payload, summary = auto_routing._same_main_path_target_retry_payload(payload, report) + + self.assertEqual(["wire-a", "wire-b"], summary["wire_uuids"]) + self.assertEqual(2, len(retry_payload["wires"])) + same_target_wire = retry_payload["wires"][0] + open_end_wire = retry_payload["wires"][1] + self.assertEqual(["QETRouteCarrier_Main"], same_target_wire["required_route_carrier_names"]) + self.assertEqual(["Main duct"], same_target_wire["required_route_carrier_labels"]) + self.assertEqual( + ["QETRouteCarrier_OpenEnd", "QETRouteCarrier_Main"], + open_end_wire["required_route_carrier_names"], + ) + self.assertEqual( + ["Main duct open end", "Main duct"], + open_end_wire["required_route_carrier_labels"], + ) + for retry_wire in retry_payload["wires"]: + self.assertEqual( + ["RoutingRange", "AuxiliaryPath"], + retry_wire["forbidden_route_carrier_kinds"], + ) + + def test_route_eplan_connections_respects_route_segment_capacity_before_detouring(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartC", "terminal-start-c", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndC", "terminal-end-c", app.Vector(100, 0, 0)) + direct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="Direct Duct", + project_uuid="project-1", + kind="WireDuct", + ) + direct.QetRouteCarrierCapacity = 2 + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], + label="Left Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Alternate Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + { + "wire_id": "wire-c", + "start_terminal_uuid": "terminal-start-c", + "end_terminal_uuid": "terminal-end-c", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + route_labels = [ + [segment["carrier"]["label"] for segment in route["route_track"]["segments"]] + for route in report["routes"] + ] + self.assertIn("Direct Duct", route_labels[0]) + self.assertIn("Direct Duct", route_labels[1]) + self.assertIn("Alternate Duct", route_labels[2]) + self.assertNotIn("Direct Duct", route_labels[2]) + + def test_route_eplan_connections_prefers_over_capacity_wire_duct_over_routing_range(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + for index in range(30): + _terminal(doc, terminal_objects, "TerminalStart{0}".format(index), "terminal-start-{0}".format(index), app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd{0}".format(index), "terminal-end-{0}".format(index), app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="Main Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + label="Panel Routing Range", + project_uuid="project-1", + kind="RoutingRange", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-{0}".format(index), + "start_terminal_uuid": "terminal-start-{0}".format(index), + "end_terminal_uuid": "terminal-end-{0}".format(index), + } + for index in range(30) + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(30, report["routed"]) + for route in report["routes"]: + labels = [ + segment["carrier"]["label"] + for segment in route["route_track"]["segments"] + ] + self.assertIn("Main Wire Duct", labels) + self.assertNotIn("Panel Routing Range", labels) + + def test_route_eplan_connections_prefers_terminal_access_target_over_nearer_routing_range(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 7000, 20), app.Vector(100, 7000, 20)], + label="Target Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + label="Near Panel Range", + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=10000.0, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-target-priority", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={ + "network_entry_max_distance": 10000.0, + "terminal_access_max_distance": 10000.0, + }, + ) + + self.assertEqual(1, report["routed"]) + route = report["routes"][0] + self.assertEqual("WireDuct", route["network"]["start_terminal_access_target_kind"]) + self.assertEqual("WireDuct", route["network"]["end_terminal_access_target_kind"]) + labels = [ + segment["carrier"]["label"] + for segment in route["route_track"]["segments"] + ] + self.assertIn("Target Wire Duct", labels) + self.assertNotIn("Near Panel Range", labels) + + def test_network_route_limits_entry_candidates_to_terminal_access_target(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 7000, 20), app.Vector(100, 7000, 20)], + label="Target Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + label="Near Panel Range", + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=10000.0, + ) + calls = {"shortest_path": 0} + original_shortest_path = routing_network.shortest_path_with_carriers + + def counted_shortest_path(*args, **kwargs): + calls["shortest_path"] += 1 + return original_shortest_path(*args, **kwargs) + + routing_network.shortest_path_with_carriers = counted_shortest_path + try: + result = auto_routing.build_network_route( + start, + end, + options={ + "network_entry_max_distance": 10000.0, + "terminal_access_max_distance": 10000.0, + }, + doc=doc, + ) + finally: + routing_network.shortest_path_with_carriers = original_shortest_path + + self.assertIsNotNone(result) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Target Wire Duct", labels) + self.assertNotIn("Near Panel Range", labels) + self.assertLessEqual(calls["shortest_path"], 1) + + def test_network_route_uses_single_candidate_on_explicit_terminal_access_target(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [ + app.Vector(0, 20, 20), + app.Vector(30, 20, 20), + app.Vector(60, 20, 20), + app.Vector(100, 20, 20), + ], + label="Multi Segment Target Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=1000.0, + ) + calls = {"shortest_path": 0} + original_shortest_path = routing_network.shortest_path_with_carriers + + def counted_shortest_path(*args, **kwargs): + calls["shortest_path"] += 1 + return original_shortest_path(*args, **kwargs) + + routing_network.shortest_path_with_carriers = counted_shortest_path + try: + result = auto_routing.build_network_route( + start, + end, + options={ + "network_entry_max_distance": 1000.0, + "terminal_access_max_distance": 1000.0, + "network_entry_candidate_limit": 4, + }, + doc=doc, + ) + finally: + routing_network.shortest_path_with_carriers = original_shortest_path + + self.assertIsNotNone(result) + self.assertEqual(1, calls["shortest_path"]) + + def test_network_route_falls_back_when_terminal_access_targets_are_disconnected(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(10, 5, 20)], + label="Disconnected Start Target", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(90, 5, 20), app.Vector(100, 5, 20)], + label="Disconnected End Target", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Fallback Panel Range", + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=50.0, + ) + + result = auto_routing.build_network_route( + start, + end, + options={ + "network_entry_max_distance": 50.0, + "terminal_access_max_distance": 50.0, + }, + doc=doc, + ) + + self.assertIsNotNone(result) + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("Fallback Panel Range", labels) + + def test_route_eplan_connections_precreates_main_path_target_bridges_before_first_route(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(10, 5, 20)], + label="Start Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(90, 5, 20), app.Vector(100, 5, 20)], + label="End Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=50.0, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-main-target-prebridge", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + captured = {"precreated_bridges": 0} + original_route_from_payload = auto_routing.route_eplan_connections_from_payload + + def fake_route_from_payload(route_doc, route_payload, *args, **kwargs): + captured["precreated_bridges"] = sum( + 1 + for carrier in routing_network.collect_route_carriers(route_doc) + if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathTargetBridge" + ) + return { + "project_uuid": "project-1", + "total_wires": 1, + "routed": 1, + "routes": [], + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, + "skipped_invalid": 0, + "errors": [], + } + + auto_routing.route_eplan_connections_from_payload = fake_route_from_payload + try: + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=False, + options={ + "auto_create_diagnostic_bridges": False, + "auto_create_main_path_detour_bridges": False, + "auto_create_terminal_access_fallback_bridges": False, + }, + ) + finally: + auto_routing.route_eplan_connections_from_payload = original_route_from_payload + + self.assertEqual(1, report["routed"]) + self.assertEqual(1, captured["precreated_bridges"]) + self.assertEqual(1, report["auto_main_path_target_bridges"]["precreated_count"]) + self.assertEqual(0, report["auto_main_path_target_bridges"]["retry_wires"]) + + def test_refresh_terminal_access_after_bridge_creation_retargets_to_nearer_user_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + far_duct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 500, 20), app.Vector(100, 500, 20)], + label="Far Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=1000.0, + ) + old_access = routing_network.terminal_access_carrier_for_terminal(terminal) + self.assertIsNotNone(old_access) + self.assertEqual(far_duct.Name, old_access.QetTerminalAccessTargetName) + self.assertGreater(old_access.QetTerminalAccessTargetDistanceMm, 400.0) + + near_bridge = routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Near User Bridge", + project_uuid="project-1", + kind="UserPath", + ) + + refresh = auto_routing._refresh_terminal_access_after_route_network_change( + doc, + project_uuid="project-1", + options={ + "terminal_exit_length": 20.0, + "terminal_exit_max_length": 80.0, + "terminal_access_max_distance": 1000.0, + }, + ) + + new_access = routing_network.terminal_access_carrier_for_terminal(terminal) + self.assertEqual(1, refresh["terminal_access_carriers"]) + self.assertIsNotNone(new_access) + self.assertEqual(near_bridge.Name, new_access.QetTerminalAccessTargetName) + self.assertLess(new_access.QetTerminalAccessTargetDistanceMm, 50.0) + + def test_route_eplan_connections_refreshes_terminal_access_after_precreated_bridge(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + far_duct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 500, 20), app.Vector(100, 500, 20)], + label="Far Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + terminal_exit_max_length=80.0, + max_distance=1000.0, + ) + old_access = routing_network.terminal_access_carrier_for_terminal(start) + self.assertEqual(far_duct.Name, old_access.QetTerminalAccessTargetName) + + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-refresh-precreated-bridge", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + captured = {"target_name": ""} + originals = { + "precreate": auto_routing._create_main_path_target_bridges_from_payload, + "route_from_payload": auto_routing.route_eplan_connections_from_payload, + "diagnose": routing_network.diagnose_routing_path_network, + "write_diag": auto_routing._write_routing_connection_batch_diagnostic, + } + + def fake_precreate(route_doc, *args, **kwargs): + near_bridge = routing_network.create_route_carrier( + route_doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Near Precreated Bridge", + project_uuid="project-1", + kind="UserPath", + ) + return { + "enabled": True, + "pairs": 1, + "created_count": 1, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": [near_bridge.Label], + "wire_uuids": ["wire-refresh-precreated-bridge"], + "rerouted": False, + "precreated_count": 1, + } + + def fake_route_from_payload(route_doc, route_payload, *args, **kwargs): + access = routing_network.terminal_access_carrier_for_terminal(start) + captured["target_name"] = getattr(access, "QetTerminalAccessTargetName", "") + return { + "project_uuid": "project-1", + "total_wires": 1, + "routed": 1, + "routes": [], + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, + "skipped_invalid": 0, + "errors": [], + } + + auto_routing._create_main_path_target_bridges_from_payload = fake_precreate + auto_routing.route_eplan_connections_from_payload = fake_route_from_payload + routing_network.diagnose_routing_path_network = lambda *args, **kwargs: {"ok": True, "issues": [], "summary": {}} + auto_routing._write_routing_connection_batch_diagnostic = lambda *args, **kwargs: None + try: + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=False, + options={ + "auto_create_diagnostic_bridges": False, + "auto_create_main_path_detour_bridges": False, + "auto_create_terminal_access_fallback_bridges": False, + }, + ) + finally: + auto_routing._create_main_path_target_bridges_from_payload = originals["precreate"] + auto_routing.route_eplan_connections_from_payload = originals["route_from_payload"] + routing_network.diagnose_routing_path_network = originals["diagnose"] + auto_routing._write_routing_connection_batch_diagnostic = originals["write_diag"] + + self.assertEqual(1, report["routed"]) + self.assertNotEqual(far_duct.Name, captured["target_name"]) + + def test_route_eplan_connections_from_payload_reuses_cached_route_graph(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="Main Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-cached-graph", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + call_count = {"value": 0} + original_build_route_graph = routing_network.build_route_graph + + def counted_build_route_graph(*args, **kwargs): + call_count["value"] += 1 + return original_build_route_graph(*args, **kwargs) + + routing_network.build_route_graph = counted_build_route_graph + try: + cache = {} + first = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"__route_network_cache": cache}, + ) + second = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"__route_network_cache": cache}, + ) + finally: + routing_network.build_route_graph = original_build_route_graph + + self.assertEqual(1, first["routed"]) + self.assertEqual(1, second["routed"]) + self.assertEqual(1, call_count["value"]) + self.assertFalse(first["route_network_reused"]) + self.assertTrue(second["route_network_reused"]) + + def test_route_eplan_connections_reuses_diagnostic_route_graph_for_initial_payload_route(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="Main Wire Duct", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-reuse-diagnostic-graph", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + call_count = {"value": 0} + original_build_route_graph = routing_network.build_route_graph + + def counted_build_route_graph(*args, **kwargs): + call_count["value"] += 1 + return original_build_route_graph(*args, **kwargs) + + routing_network.build_route_graph = counted_build_route_graph + try: + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=False, + options={ + "auto_create_diagnostic_bridges": False, + "auto_create_main_path_target_bridges": False, + "auto_create_main_path_detour_bridges": False, + "auto_create_terminal_access_fallback_bridges": False, + }, + ) + finally: + routing_network.build_route_graph = original_build_route_graph + + self.assertEqual(1, report["routed"]) + self.assertEqual(1, call_count["value"]) + self.assertTrue(report["route_network_reused"]) + + def test_generate_eplan_routing_path_network_skips_prepare_existing_network_summary(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + call_count = {"value": 0} + original_build_route_graph = routing_network.build_route_graph + + def counted_build_route_graph(*args, **kwargs): + call_count["value"] += 1 + return original_build_route_graph(*args, **kwargs) + + routing_network.build_route_graph = counted_build_route_graph + try: + report = auto_routing.generate_eplan_routing_path_network( + doc, + project_uuid="project-1", + ) + finally: + routing_network.build_route_graph = original_build_route_graph + + self.assertIn("layout_space", report) + self.assertNotIn("existing_network", report["layout_space"]) + self.assertEqual(2, call_count["value"]) + + def test_route_eplan_connections_does_not_refresh_full_network_after_auto_bridge(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + doc = FakeDocument() payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, - { - "wire_id": "wire-c", - "start_terminal_uuid": "terminal-start-c", - "end_terminal_uuid": "terminal-end-c", - }, + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } ], } + update_calls = {"value": 0} + route_calls = {"value": 0} + originals = { + "update": auto_routing.update_eplan_routing_path_network, + "diagnose": routing_network.diagnose_routing_path_network, + "diagnostic_bridges": routing_network.create_user_path_bridges_from_diagnostic_suggestions, + "route_from_payload": auto_routing.route_eplan_connections_from_payload, + "target_bridges": auto_routing._create_main_path_target_bridges_from_report, + "write_diag": auto_routing._write_routing_connection_batch_diagnostic, + } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + def fake_update(*args, **kwargs): + update_calls["value"] += 1 + return {"updated": True} - route_labels = [ - [segment["carrier"]["label"] for segment in route["route_track"]["segments"]] - for route in report["routes"] - ] - self.assertIn("Direct Duct", route_labels[0]) - self.assertIn("Direct Duct", route_labels[1]) - self.assertIn("Alternate Duct", route_labels[2]) - self.assertNotIn("Direct Duct", route_labels[2]) + def fake_route_from_payload(*args, **kwargs): + route_calls["value"] += 1 + carrier_kind = "RoutingRange" if route_calls["value"] == 1 else "WireDuct" + return { + "total_wires": 1, + "routed": 1, + "routes": [ + { + "wire_uuid": "wire-a", + "route_status": "OK", + "length_mm": 10.0, + "wire_style_status": "Resolved", + "route_track": { + "segments": [ + {"carrier": {"kind": carrier_kind, "label": carrier_kind}} + ] + }, + "network": {}, + } + ], + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, + "skipped_invalid": 0, + "errors": [], + } + + auto_routing.update_eplan_routing_path_network = fake_update + routing_network.diagnose_routing_path_network = lambda *args, **kwargs: {"ok": True, "issues": [], "summary": {}} + routing_network.create_user_path_bridges_from_diagnostic_suggestions = lambda *args, **kwargs: {"created": []} + auto_routing.route_eplan_connections_from_payload = fake_route_from_payload + auto_routing._create_main_path_target_bridges_from_report = lambda *args, **kwargs: { + "enabled": True, + "pairs": 1, + "created_count": 1, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": ["A -> B"], + "wire_uuids": ["wire-a"], + "rerouted": False, + } + auto_routing._write_routing_connection_batch_diagnostic = lambda *args, **kwargs: None + try: + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=True, + options={ + "auto_create_main_path_detour_bridges": False, + "auto_create_terminal_access_fallback_bridges": False, + }, + ) + finally: + auto_routing.update_eplan_routing_path_network = originals["update"] + routing_network.diagnose_routing_path_network = originals["diagnose"] + routing_network.create_user_path_bridges_from_diagnostic_suggestions = originals["diagnostic_bridges"] + auto_routing.route_eplan_connections_from_payload = originals["route_from_payload"] + auto_routing._create_main_path_target_bridges_from_report = originals["target_bridges"] + auto_routing._write_routing_connection_batch_diagnostic = originals["write_diag"] + + self.assertEqual(1, report["routed"]) + self.assertEqual(2, route_calls["value"]) + self.assertEqual(1, update_calls["value"]) def test_route_eplan_connections_prefers_unused_segments_occupied_by_existing_wires(self): _install_fake_freecad() @@ -17077,6 +19376,67 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("routing_errors", payload["issue_codes"]) self.assertIn("错误 71 条", message) + def test_batch_issue_codes_ignore_candidate_debug_warnings_when_final_routes_are_clean(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 2, + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, + "skipped_invalid": 0, + "route_status_counts": {"Routed": 2}, + "routes": [ + { + "wire_uuid": "wire-candidate-risk", + "wire_label": "N1", + "route_status": "Routed", + "network": {"route_candidate_obstacle_hits": 2}, + "route_track": { + "segments": [ + { + "carrier": { + "kind": "WireDuct", + "label": "主线槽A", + "capacity": 1, + } + } + ] + }, + "lane": {"index": 1}, + }, + { + "wire_uuid": "wire-clean", + "wire_label": "N2", + "route_status": "Routed", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "WireDuct", + "label": "主线槽A", + "capacity": 1, + } + } + ] + }, + "lane": {"index": 0}, + }, + ], + } + + issue_codes = auto_routing._routing_connection_batch_issue_codes(report) + payload = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual([], issue_codes) + self.assertEqual([], payload["issue_codes"]) + self.assertEqual(1, payload["route_candidate_obstacle_warning_count"]) + self.assertEqual(1, payload["route_capacity_pressure_warning_count"]) + self.assertNotIn("接入避障提示", message) + self.assertNotIn("容量提示", message) + def test_route_eplan_connections_report_includes_missing_route_network_sample(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -19078,6 +21438,177 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("Available", report["wire_style_database"]["status"]) self.assertEqual(1, report["wire_style"]["resolved"]) + def test_routing_preflight_discovers_style_database_from_saved_fcstd_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + project_dir = Path(temp_dir) / "project-a" + exchange_dir = project_dir / ".qet_freecad" + data_dir = project_dir / "datafiles" + exchange_dir.mkdir(parents=True) + data_dir.mkdir(parents=True) + doc.FileName = str(exchange_dir / "QETScene.FCStd") + db_path = data_dir / "project-local.db" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT, + name TEXT, + line_color TEXT + ) + """ + ) + connection.execute( + "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", + (1, "project-1", "黑色控制线", "#000000"), + ) + connection.commit() + finally: + connection.close() + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "1", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + + self.assertTrue(report["ok"]) + self.assertEqual(str(db_path), report["wire_style_database"]["path"]) + self.assertEqual("Available", report["wire_style_database"]["status"]) + self.assertEqual(1, report["wire_style"]["resolved"]) + + def test_route_eplan_connections_loads_v2_device_terminals_from_saved_fcstd_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 50, 0)) + terminal_objects.set_terminal_semantics( + start, + "project-1", + "device-start", + "terminal-start", + "instance-start", + label="S1", + ) + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_shared") + root.addObject(device) + terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-shared") + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-shared") + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="instance-shared", + ) + for name, point in ( + ("QETTerminal_shared_a", app.Vector(120, 0, 0)), + ("QETTerminal_shared_b", app.Vector(120, 50, 0)), + ): + terminal = terminal_objects.create_lcs_object( + doc, + name, + placement=app.Placement(point, app.Rotation()), + label="old", + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "wrong-element", + "shared-terminal", + "instance-shared", + label="old", + slot_name="old", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 50, 20), app.Vector(120, 50, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + project_dir = Path(temp_dir) / "project-a" + exchange_dir = project_dir / ".qet_freecad" + exchange_dir.mkdir(parents=True) + doc.FileName = str(exchange_dir / "QETScene.FCStd") + (exchange_dir / "2d_to_3d.json").write_text( + json.dumps( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-shared", + "terminals": [ + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-a", + "terminal_instance_id": "instance-shared", + "terminal_display": "A1", + }, + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-b", + "terminal_instance_id": "instance-shared", + "terminal_display": "B1", + }, + ], + } + ], + "wires": [], + } + ), + encoding="utf-8", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-from-task-payload", + "start_element_uuid": "device-start", + "start_instance_id": "instance-start", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "S1", + "end_element_uuid": "element-b", + "end_instance_id": "instance-shared", + "end_terminal_uuid": "shared-terminal", + "end_terminal_display": "B1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(1, report["routed"]) + self.assertEqual(2, report["repaired_duplicate_terminal_metadata"]) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = list(getattr(routed_group, "Group", []) or [])[0] + self.assertEqual("element-b", wire.QetEndElementUuid) + self.assertEqual("B1", wire.QetEndTerminalDisplay) + def test_bind_wire_task_terminals_from_payload_does_not_create_wires(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -19310,6 +21841,38 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual([], routing_network.collect_route_carriers(doc)) self.assertIn(wire, wiring_objects.ensure_routed_group(doc, "project-1").Group) + def test_apply_phase1_acceptance_view_hides_carriers_and_shows_routed_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + ) + wire = auto_routing.route_eplan_connection_between_terminals(doc, start, end)["wire"] + wire.ViewObject.Visibility = False + carrier.ViewObject.Visibility = True + terminal_objects.ensure_string_property( + wire, + "QetWireStyleJson", + "QET Wiring", + "", + json.dumps({"line_color": "#000000", "line_width": 2.5}), + ) + + report = auto_routing.apply_phase1_acceptance_view(doc) + + self.assertTrue(wire.ViewObject.Visibility) + self.assertFalse(carrier.ViewObject.Visibility) + self.assertEqual(1, report["routed_wire_visibility"]["visible"]) + self.assertEqual(0, report["route_carrier_visibility"]["visible_after_hide"]) + self.assertEqual(1, report["wire_style_application"]["styled_black"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/python/freecad_exchange_device_import_fcstd_test.py b/tests/python/freecad_exchange_device_import_fcstd_test.py index 7e1d917..655028f 100644 --- a/tests/python/freecad_exchange_device_import_fcstd_test.py +++ b/tests/python/freecad_exchange_device_import_fcstd_test.py @@ -948,6 +948,53 @@ class FcstdDeviceImportTest(unittest.TestCase): self.assertEqual("Placed", device_group.QetAssemblyState) self.assertEqual(["DeviceBody"], [obj.Name for obj in device_group.Group if obj.Name == "DeviceBody"]) + def test_insert_pending_device_does_not_treat_existing_engineering_terminals_as_model_objects(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "device.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root, + "element-a", + "device-inst-1", + str(model_path), + "A6300-2", + 0, + ) + device_import._set_device_assembly_state( + device_group, + device_import.ASSEMBLY_STATE_PENDING, + ) + terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_existing") + terminal.Role = "Terminal" + terminal.addProperty("App::PropertyString", "QetTerminalUuid", "QET Exchange", "") + terminal.QetTerminalUuid = "terminal-a" + terminal.addProperty("App::PropertyBool", "CanWire", "QET Exchange", "") + terminal.CanWire = True + device_group.addObject(terminal) + + import_calls = [] + + def fake_import_model(doc_arg, group_arg, path_arg, **kwargs): + import_calls.append((doc_arg, group_arg, path_arg, kwargs)) + body = doc_arg.addObject("Part::Feature", "DeviceBody") + group_arg.addObject(body) + return [body] + + device_import._import_model_into_group = fake_import_model + + result = device_import.insert_pending_device(doc, device_group) + + self.assertEqual(1, len(import_calls)) + self.assertFalse(result["already_placed"]) + self.assertEqual(["DeviceBody"], [obj.Name for obj in result["imported_objects"]]) + def test_insert_pending_device_can_place_whole_device_on_mount_target(self): with tempfile.TemporaryDirectory() as temp_dir: model_path = Path(temp_dir) / "device.step" @@ -1201,6 +1248,171 @@ class FcstdDeviceImportTest(unittest.TestCase): self.assertIn("jhd5.FCStd", rows[0]["display_text"]) self.assertIn("QET_Exchange_OpenPendingDevicePanel", registered) + def test_pending_device_panel_focuses_imported_objects_after_insert(self): + _install_fake_freecad(None) + gui = sys.modules["FreeCADGui"] + selection_calls = [] + view_messages = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selection_calls.append(("clear",)), + addSelection=lambda doc_name, obj_name: selection_calls.append((doc_name, obj_name)), + ) + gui.SendMsgToActiveView = lambda message: view_messages.append(message) + + device_import, _ = _reload_modules() + sys.modules.pop("PendingDeviceAssemblyPanel", None) + pending_panel = importlib.import_module("PendingDeviceAssemblyPanel") + + doc = FakeDocument("QETScene") + device = doc.addObject("App::Part", "QETDevice_A6300_2") + body = doc.addObject("Part::Feature", "Solid") + body.ViewObject.Visibility = False + + pending_panel.focus_inserted_result(doc, {"device": device, "imported_objects": [body]}) + + self.assertTrue(body.ViewObject.Visibility) + self.assertEqual([("clear",), ("QETScene", "Solid")], selection_calls) + self.assertEqual(["ViewSelection"], view_messages) + + def test_pending_device_panel_rejects_insert_result_without_visible_model_objects(self): + _install_fake_freecad(None) + _reload_modules() + sys.modules.pop("PendingDeviceAssemblyPanel", None) + pending_panel = importlib.import_module("PendingDeviceAssemblyPanel") + + doc = FakeDocument("QETScene") + device = doc.addObject("App::Part", "QETDevice_A6300_2") + + with self.assertRaisesRegex( + pending_panel.PendingDeviceAssemblyPanelError, + "没有导入任何可显示模型对象", + ): + pending_panel.focus_inserted_result(doc, {"device": device, "imported_objects": []}) + + def test_pending_device_panel_syncs_engineering_terminals_after_insert(self): + _install_fake_freecad(None) + app = sys.modules["FreeCAD"] + device_import, _ = _reload_modules() + sys.modules.pop("PendingDeviceAssemblyPanel", None) + pending_panel = importlib.import_module("PendingDeviceAssemblyPanel") + + doc = FakeDocument("QETScene") + app.ActiveDocument = doc + app._qet_exchange_payload = {"project_uuid": "project-1", "devices": []} + device = doc.addObject("App::Part", "QETDevice_A6300_2") + body = doc.addObject("Part::Feature", "Solid") + calls = [] + + def fake_insert(insert_doc, insert_device, **kwargs): + calls.append(("insert", insert_doc, insert_device, kwargs)) + return {"device": insert_device, "imported_objects": [body]} + + class FakeTerminalImport: + @staticmethod + def import_terminals_from_payload(payload, scene_path=""): + calls.append(("terminals", payload, scene_path)) + return {"imported_terminals": 1, "updated_terminals": 2, "warnings": []} + + original_insert = device_import.insert_pending_device + original_terminal_import = pending_panel.TerminalImport + try: + device_import.insert_pending_device = fake_insert + pending_panel.TerminalImport = FakeTerminalImport + + result = pending_panel.insert_device_and_sync_terminals( + doc, + device, + mount_offset_mm=20.0, + ) + finally: + device_import.insert_pending_device = original_insert + pending_panel.TerminalImport = original_terminal_import + + self.assertIs(device, result["device"]) + self.assertEqual([body], result["imported_objects"]) + self.assertEqual({"imported_terminals": 1, "updated_terminals": 2, "warnings": []}, result["terminal_report"]) + self.assertEqual("insert", calls[0][0]) + self.assertEqual("terminals", calls[1][0]) + self.assertEqual(app._qet_exchange_payload, calls[1][1]) + + def test_pending_device_panel_batch_inserts_same_prefix_qet_devices_to_target(self): + _install_fake_freecad(None) + app = sys.modules["FreeCAD"] + device_import, _template_semantics = _reload_modules() + terminal_objects = importlib.import_module("TerminalObjects") + sys.modules.pop("PendingDeviceAssemblyPanel", None) + pending_panel = importlib.import_module("PendingDeviceAssemblyPanel") + + doc = FakeDocument("QETScene") + app.ActiveDocument = doc + app._qet_exchange_payload = {"project_uuid": "project-1", "devices": []} + root = device_import._ensure_root_group(doc, None, "project-1") + + def pending_device(element_uuid, instance_id, display_tag): + device, _created = device_import._ensure_device_group( + doc, + root, + element_uuid, + instance_id, + r"D:\models\terminal.FCStd", + display_tag, + 0, + ) + device_import._set_device_assembly_state( + device, + device_import.ASSEMBLY_STATE_PENDING, + ) + return device + + ud2 = pending_device("element-ud2", "instance-ud2", "UD:2") + pending_device("element-id1", "instance-id1", "ID:1") + pending_device("element-ud1", "instance-ud1", "UD:1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + calls = [] + body = doc.addObject("Part::Feature", "ImportedBody") + + def fake_insert(insert_doc, insert_device, **kwargs): + calls.append((getattr(insert_device, "QetDisplayTag", ""), kwargs)) + return {"device": insert_device, "imported_objects": [body]} + + class FakeTerminalImport: + @staticmethod + def import_terminals_from_payload(payload, scene_path=""): + calls.append(("terminals", payload, scene_path)) + return {"imported_terminals": 2, "updated_terminals": 0, "warnings": []} + + original_insert = device_import.insert_pending_device + original_terminal_import = pending_panel.TerminalImport + try: + device_import.insert_pending_device = fake_insert + pending_panel.TerminalImport = FakeTerminalImport + + report = pending_panel.insert_matching_pending_batch_to_target( + doc, + ud2, + rail, + pitch_mm=5.2, + start_offset_mm=1.0, + mount_offset_mm=20.0, + ) + finally: + device_import.insert_pending_device = original_insert + pending_panel.TerminalImport = original_terminal_import + + self.assertEqual(["UD:1", "UD:2"], [call[0] for call in calls[:2]]) + self.assertEqual("terminals", calls[2][0]) + self.assertEqual(2, len(report["devices"])) + self.assertEqual("UD", report["batch_prefix"]) + self.assertEqual(["UD:1", "UD:2"], report["device_labels"]) + self.assertEqual(2, report["inserted_count"]) + self.assertEqual({"imported_terminals": 2, "updated_terminals": 0, "warnings": []}, report["terminal_report"]) + self.assertAlmostEqual(1.0, calls[0][1]["mount_placement"].Base.x - rail.Placement.Base.x) + self.assertAlmostEqual(6.2, calls[1][1]["mount_placement"].Base.x - rail.Placement.Base.x) + def test_import_devices_from_payload_reuses_fcstd_source_document_within_one_sync(self): source = FakeDocument("TerminalSlice", r"D:\models\qet_terminal_slice.FCStd") source.addObject("Part::Feature", "Body") diff --git a/tests/python/freecad_exchange_terminal_import_template_slots_test.py b/tests/python/freecad_exchange_terminal_import_template_slots_test.py index 92aa105..96572df 100644 --- a/tests/python/freecad_exchange_terminal_import_template_slots_test.py +++ b/tests/python/freecad_exchange_terminal_import_template_slots_test.py @@ -192,6 +192,426 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual("generated_bbox_fallback", terminals[0].QetTerminalGeometrySource) self.assertFalse(terminals[0].ViewObject.Visibility) + def test_import_repairs_duplicate_terminal_instance_ids(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "device-instance-a", + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-instance-a", + "terminals": [ + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-a", + "terminal_instance_id": "shared-instance", + "terminal_display": "A1", + }, + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-b", + "terminal_instance_id": "shared-instance", + "terminal_display": "B1", + }, + ], + } + ], + } + ) + + terminal_group = terminal_objects.find_child_group_by_kind( + device, + terminal_objects.TERMINAL_GROUP_KIND, + ) + terminals = terminal_objects.collect_terminal_objects(terminal_group) + terminal_instance_ids = [terminal.QetTerminalInstanceId for terminal in terminals] + + self.assertEqual(2, report["imported_terminals"]) + self.assertEqual(2, report["repaired_terminal_instance_ids"]) + self.assertEqual(2, len(set(terminal_instance_ids))) + self.assertNotIn("shared-instance", terminal_instance_ids) + self.assertEqual({"element-a", "element-b"}, {terminal.QetElementUuid for terminal in terminals}) + + def test_repaired_terminal_instance_ids_are_stable_when_payload_order_changes(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + def import_and_collect(entries): + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "device-instance-a", + ) + + terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-instance-a", + "terminals": entries, + } + ], + } + ) + terminal_group = terminal_objects.find_child_group_by_kind( + device, + terminal_objects.TERMINAL_GROUP_KIND, + ) + return { + (terminal.QetElementUuid, terminal.Label): terminal.QetTerminalInstanceId + for terminal in terminal_objects.collect_terminal_objects(terminal_group) + } + + first_order = [ + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-a", + "terminal_instance_id": "shared-instance", + "terminal_display": "A1", + }, + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-b", + "terminal_instance_id": "shared-instance", + "terminal_display": "B1", + }, + ] + second_order = list(reversed(first_order)) + + self.assertEqual( + import_and_collect(first_order), + import_and_collect(second_order), + ) + + def test_import_does_not_treat_existing_terminal_object_as_parent_device(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "element-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "device-instance-a", + ) + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="device-instance-a", + ) + stale_terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_Stale") + terminal_group.addObject(stale_terminal) + terminal_objects.set_terminal_semantics( + stale_terminal, + "project-1", + "element-b", + "terminal-old", + "device-instance-a", + label="old", + slot_name="old", + terminal_instance_id="terminal-old-instance", + terminal_display="old", + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-b", + "element_uuid": "element-b", + "terminal_instance_id": "terminal-instance-b", + "terminal_display": "B1", + }, + ], + } + ], + } + ) + + terminals = terminal_objects.collect_terminal_objects(terminal_group) + + self.assertEqual(1, report["imported_terminals"]) + self.assertEqual( + {("element-b", "terminal-b", "B1")}, + { + ( + terminal.QetElementUuid, + terminal.QetTerminalUuid, + terminal.QetTerminalDisplay, + ) + for terminal in terminals + }, + ) + + def test_reimport_creates_new_terminal_when_existing_uuid_belongs_to_other_element(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "element-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "device-instance-a", + ) + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="device-instance-a", + ) + old_terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_Shared") + terminal_group.addObject(old_terminal) + terminal_objects.set_terminal_semantics( + old_terminal, + "project-1", + "element-a", + "shared-terminal", + "device-instance-a", + label="A1", + slot_name="A1", + terminal_instance_id="old-terminal-instance", + terminal_display="A1", + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-instance-a", + "terminals": [ + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-a", + "terminal_instance_id": "old-terminal-instance", + "terminal_display": "A1", + }, + { + "terminal_uuid": "shared-terminal", + "element_uuid": "element-b", + "terminal_instance_id": "new-terminal-instance", + "terminal_display": "B1", + }, + ], + } + ], + } + ) + + terminals = terminal_objects.collect_terminal_objects(terminal_group) + + self.assertEqual(1, report["imported_terminals"]) + self.assertEqual(1, report["updated_terminals"]) + self.assertEqual( + {("element-a", "shared-terminal", "A1"), ("element-b", "shared-terminal", "B1")}, + { + ( + terminal.QetElementUuid, + terminal.QetTerminalUuid, + terminal.QetTerminalDisplay, + ) + for terminal in terminals + }, + ) + + def test_reimport_removes_stale_duplicate_qet_terminal_contexts(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "element-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "device-instance-a", + ) + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="device-instance-a", + ) + stale_duplicate = terminal_objects.create_lcs_object(doc, "QETTerminal_StaleDuplicate") + terminal_group.addObject(stale_duplicate) + terminal_objects.set_terminal_semantics( + stale_duplicate, + "project-1", + "element-a", + "terminal-a", + "device-instance-a", + label="A1-old", + slot_name="A1", + terminal_instance_id="stale-terminal-instance", + terminal_display="A1", + ) + stale_not_in_payload = terminal_objects.create_lcs_object(doc, "QETTerminal_StaleMissing") + terminal_group.addObject(stale_not_in_payload) + terminal_objects.set_terminal_semantics( + stale_not_in_payload, + "project-1", + "element-c", + "terminal-c", + "device-instance-a", + label="C1", + slot_name="C1", + terminal_instance_id="stale-missing-instance", + terminal_display="C1", + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "device-instance-a", + "terminals": [ + { + "terminal_uuid": "terminal-a", + "element_uuid": "element-a", + "terminal_instance_id": "terminal-instance-a", + "terminal_display": "A1", + }, + { + "terminal_uuid": "terminal-b", + "element_uuid": "element-b", + "terminal_instance_id": "terminal-instance-b", + "terminal_display": "B1", + }, + ], + } + ], + } + ) + + terminals = terminal_objects.collect_terminal_objects(terminal_group) + + self.assertEqual(1, report["removed_terminals"]) + self.assertEqual( + {("element-a", "terminal-a", "A1"), ("element-b", "terminal-b", "B1")}, + { + ( + terminal.QetElementUuid, + terminal.QetTerminalUuid, + terminal.QetTerminalDisplay, + ) + for terminal in terminals + }, + ) + def test_import_preserves_local_terminals_when_payload_has_no_entry_for_device(self): _install_fake_freecad() terminal_import, terminal_objects, device_import = _reload_modules() @@ -264,6 +684,74 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual([local_terminal], terminals) self.assertEqual("local:instance-a:P1", local_terminal.QetTerminalUuid) + def test_import_preserves_existing_qet_terminals_when_payload_has_no_entry_for_device(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "instance-a", + ) + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="instance-a", + ) + existing_terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_Existing") + terminal_group.addObject(existing_terminal) + terminal_objects.set_terminal_semantics( + existing_terminal, + "project-1", + "device-a", + "terminal-a", + "instance-a", + label="P1", + slot_name="P1", + terminal_instance_id="terminal-instance-a", + terminal_display="P1", + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-a", + "terminals": [], + } + ], + } + ) + + terminals = terminal_objects.collect_terminal_objects(terminal_group) + + self.assertEqual(0, report["removed_terminals"]) + self.assertEqual([existing_terminal], terminals) + self.assertEqual("terminal-a", existing_terminal.QetTerminalUuid) + def test_import_accepts_nested_device_terminals_without_top_level_terminals(self): _install_fake_freecad() terminal_import, terminal_objects, device_import = _reload_modules() diff --git a/tests/python/freecad_exchange_wiring_test.py b/tests/python/freecad_exchange_wiring_test.py index 5eddf26..c09c7d6 100644 --- a/tests/python/freecad_exchange_wiring_test.py +++ b/tests/python/freecad_exchange_wiring_test.py @@ -560,6 +560,65 @@ class WiringTest(unittest.TestCase): label="P1", ) + with tempfile.TemporaryDirectory() as tmp_dir: + report = write_back.write_back_document( + doc, + scene_path=str(Path(tmp_dir) / "scene.FCStd"), + payload={"project_uuid": "project-1"}, + ) + + self.assertEqual(1, len(report["terminals"])) + self.assertEqual("terminal-a", report["terminals"][0]["terminal_uuid"]) + self.assertEqual("instance-a", report["terminals"][0]["device_instance_id"]) + self.assertNotEqual("instance-a", report["terminals"][0]["terminal_instance_id"]) + + def test_writeback_prefers_terminal_instance_id_property(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _manual_wiring, write_back = _reload_modules() + + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "device-instance-a", + ) + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id="device-instance-a", + ) + + terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_A") + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + "device-a", + "terminal-a", + "device-instance-a", + label="A", + ) + terminal_objects.ensure_string_property( + terminal, + "QetTerminalInstanceId", + "QET Exchange", + "Stable 3D terminal instance UUID", + "terminal-instance-a", + ) + with tempfile.TemporaryDirectory() as tmp_dir: report = write_back.write_back_document( doc, @@ -570,8 +629,8 @@ class WiringTest(unittest.TestCase): self.assertEqual( [{ "terminal_uuid": "terminal-a", - "device_instance_id": "instance-a", - "terminal_instance_id": "instance-a", + "device_instance_id": "device-instance-a", + "terminal_instance_id": "terminal-instance-a", }], report["terminals"], ) @@ -642,6 +701,132 @@ class WiringTest(unittest.TestCase): report["instances"], ) + def test_writeback_syncs_terminals_from_payload_before_collecting_bindings(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _manual_wiring, write_back = _reload_modules() + + doc = FakeDocument() + root = terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "instance-a", + ) + + def sync_from_payload(payload, _scene_path=""): + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid=payload["project_uuid"], + instance_id="instance-a", + ) + terminal = terminal_objects.create_lcs_object(doc, "QETTerminal_B") + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + payload["project_uuid"], + "device-b", + "terminal-b", + "instance-a", + label="B", + terminal_instance_id="terminal-instance-b", + terminal_display="B", + ) + return {"imported_terminals": 1} + + write_back.TerminalImport = types.SimpleNamespace( + import_terminals_from_payload=sync_from_payload + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + report = write_back.write_back_document( + doc, + scene_path=str(Path(tmp_dir) / "scene.FCStd"), + payload={ + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-a", + "terminals": [ + { + "element_uuid": "device-b", + "terminal_uuid": "terminal-b", + "terminal_display": "B", + } + ], + } + ], + }, + ) + + self.assertEqual( + [ + {"element_uuid": "device-a", "device_instance_id": "instance-a"}, + {"element_uuid": "device-b", "device_instance_id": "instance-a"}, + ], + report["instances"], + ) + + def test_writeback_observer_syncs_terminals_before_document_save(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, _manual_wiring, write_back = _reload_modules() + + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + calls = [] + + class FakeTerminalImport: + @staticmethod + def import_terminals_from_payload(payload, scene_path=""): + calls.append((payload, scene_path)) + return {"imported_terminals": 1} + + original_terminal_import = write_back.TerminalImport + try: + write_back.TerminalImport = FakeTerminalImport + with tempfile.TemporaryDirectory() as tmp_dir: + scene_path = Path(tmp_dir) / "scene.FCStd" + payload_path = Path(tmp_dir) / "2d_to_3d.json" + payload_path.write_text( + json.dumps( + { + "project_uuid": "project-1", + "devices": [ + { + "device_instance_id": "instance-a", + "terminals": [ + { + "element_uuid": "device-a", + "terminal_uuid": "terminal-a", + } + ], + } + ], + } + ), + encoding="utf-8", + ) + + observer = write_back._WriteBackObserver() + observer.slotStartSaveDocument(doc, str(scene_path)) + finally: + write_back.TerminalImport = original_terminal_import + + self.assertEqual(1, len(calls)) + self.assertEqual("project-1", calls[0][0]["project_uuid"]) + self.assertEqual(str(scene_path), calls[0][1]) + if __name__ == "__main__": unittest.main()