From f78f30509fb0a3e129f24609535db5868dcac998 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Thu, 18 Jun 2026 09:56:42 +0800 Subject: [PATCH] =?UTF-8?q?feature/=E5=AE=8C=E5=96=84=E5=BE=85=E8=A3=85?= =?UTF-8?q?=E9=85=8D=E8=AE=BE=E5=A4=87=E5=92=8C=E5=B8=83=E7=BA=BF-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()