From bb30eec80bf3e6c2f9046a638b0c209d81a5d772 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Fri, 29 May 2026 10:33:47 +0800 Subject: [PATCH 01/63] =?UTF-8?q?fix:=20=E9=99=8D=E4=BD=8EFreeCAD=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=A2=B0=E6=92=9E=E8=AF=AF=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 78 ++++++++++++++++++- .../freecad_exchange_auto_routing_test.py | 60 ++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index bfba5e7..6dff20c 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -46,6 +46,8 @@ DEFAULT_OPTIONS = { "allow_floating_fallback": False, # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 "obstacle_clearance": 5.0, + # 端子出线/入线段通常会贴近端子塑壳或设备外壳,不作为主路径碰撞判定依据。 + "ignore_endpoint_collision_segments": True, # 防止坐标异常或端子离路由网络过远时生成超长接入线,把 FreeCAD # 视图包围盒拉得过大,导致旋转时模型被裁剪到看不见。 "terminal_access_max_distance": 1000.0, @@ -800,10 +802,32 @@ def _expanded_obstacle_exclusion_ids(doc, exclude): return excluded +def _distance_point_to_bbox(point, bbox): + squared = 0.0 + for axis, min_key, max_key in ( + ("x", "xmin", "xmax"), + ("y", "ymin", "ymax"), + ("z", "zmin", "zmax"), + ): + value = _axis_value(point, axis) + low = float(bbox[min_key]) + high = float(bbox[max_key]) + if value < low: + squared += (low - value) * (low - value) + elif value > high: + squared += (value - high) * (value - high) + return math.sqrt(squared) + + def collect_obstacles(doc, exclude=None, options=None): opts = _merged_options(options) excluded = _expanded_obstacle_exclusion_ids(doc, exclude) clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0) + endpoint_clearance = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) + clearance + endpoint_points = [] + for obj in exclude or []: + if obj is not None and TerminalObjects.is_terminal_object(obj): + endpoint_points.append(_terminal_origin(obj)) obstacles = [] for obj in list(getattr(doc, "Objects", []) or []): if id(obj) in excluded: @@ -820,6 +844,11 @@ def collect_obstacles(doc, exclude=None, options=None): bbox = _bbox_payload(obj, clearance=clearance) if bbox is None: continue + if endpoint_points and any( + _distance_point_to_bbox(point, bbox) <= endpoint_clearance + for point in endpoint_points + ): + continue obstacles.append( { "name": getattr(obj, "Name", ""), @@ -861,9 +890,12 @@ def _segment_intersects_bbox(start, end, bbox): return True -def detect_collisions(points, obstacles): +def detect_collisions(points, obstacles, ignored_segment_indices=None): + ignored = set(ignored_segment_indices or []) collisions = [] for index in range(max(len(points) - 1, 0)): + if index in ignored: + continue start = points[index] end = points[index + 1] for obstacle in obstacles: @@ -878,6 +910,16 @@ def detect_collisions(points, obstacles): return collisions +def _endpoint_collision_segment_indices(points): + segment_count = max(len(points or []) - 1, 0) + if segment_count <= 0: + return set() + ignored = {0} + if segment_count > 1: + ignored.add(segment_count - 1) + return ignored + + def _detach_object_from_groups(doc, obj): parents = list(getattr(obj, "InList", []) or []) parents.extend(list(getattr(doc, "Objects", []) or [])) @@ -1026,7 +1068,10 @@ def route_between_terminals( raise AutoRoutingError("Auto-routing produced fewer than two points.") obstacles = collect_obstacles(doc, exclude=[start_terminal, end_terminal], options=opts) - collisions = detect_collisions(points, obstacles) + ignored_collision_segments = set() + if opts.get("ignore_endpoint_collision_segments", True): + ignored_collision_segments = _endpoint_collision_segment_indices(points) + collisions = detect_collisions(points, obstacles, ignored_segment_indices=ignored_collision_segments) status = "CollisionWarning" if collisions else "Routed" wire_name = _unique_name(doc, _wire_object_name(start_terminal, end_terminal, wire_uuid)) @@ -1161,6 +1206,7 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): "skipped_invalid": 0, "missing_endpoint_uuids": [], "missing_endpoint_samples": [], + "collision_samples": [], "errors": [], "routes": [], } @@ -1218,6 +1264,20 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): continue if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 + route_collision_samples = [] + for collision in list(result.get("collisions", []) or [])[:3]: + sample = dict(collision) + sample.update( + { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "start_terminal_uuid": start_uuid, + "end_terminal_uuid": end_uuid, + } + ) + route_collision_samples.append(sample) + if len(report["collision_samples"]) < 8: + report["collision_samples"].append(sample) report["routed"] += 1 route_length = float(result.get("length_mm", 0.0) or 0.0) report["total_length_mm"] += route_length @@ -1234,6 +1294,7 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): "lane": result.get("lane", {}), "network": result.get("network", {}), "collision_count": result["collision_count"], + "collision_samples": route_collision_samples, } ) report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) @@ -1260,6 +1321,19 @@ def format_route_all_report(report): errors = report.get("errors", []) or [] if errors: message += "\n首个错误:{0}".format(str(errors[0])) + collision_sample = (report.get("collision_samples") or [None])[0] + if collision_sample: + obstacle_text = ( + collision_sample.get("obstacle_label") + or collision_sample.get("obstacle_name") + or "未知对象" + ) + message += "\n碰撞示例:导线 {0} 碰到 {1}。".format( + collision_sample.get("wire_label") + or collision_sample.get("wire_uuid") + or "未知导线", + obstacle_text, + ) auto_bound = report.get("auto_bound_terminals", 0) auto_created = report.get("auto_created_terminals", 0) if auto_bound or auto_created: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index b218a3d..37b6d37 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -976,6 +976,27 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("CollisionWarning", result["wire"].RouteStatus) self.assertEqual(1, result["collision_count"]) + def test_auto_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") + 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", + ) + terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") + terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) + + result = auto_routing.route_between_terminals(doc, start, end) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + def test_auto_route_ignores_endpoint_device_body_as_obstacle(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -1132,6 +1153,45 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("network-dijkstra-v1", route["algorithm"]) self.assertEqual(1, route["network"]["carriers"]) + def test_route_all_report_includes_collision_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)) + _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", "MiddleObstacle") + obstacle.Label = "Middle Obstacle" + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_all_from_payload(doc, payload) + message = auto_routing.format_route_all_report(report) + + self.assertEqual(1, report["collision_warnings"]) + self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) + self.assertEqual("N4111", report["collision_samples"][0]["wire_label"]) + self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) + self.assertEqual("Middle Obstacle", report["routes"][0]["collision_samples"][0]["obstacle_label"]) + self.assertIn("碰撞示例", message) + self.assertIn("Middle Obstacle", message) + def test_route_all_report_calls_out_local_unbound_terminals(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() From 0a753ffbd694c447410b4cc76f7191b79b7722f0 Mon Sep 17 00:00:00 2001 From: qiudejia Date: Fri, 29 May 2026 16:46:52 +0800 Subject: [PATCH 02/63] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=B8=83=E7=BA=BF=E5=8A=9F=E8=83=BD=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 86 +- docs/FreeCAD 机柜装配操作文档.md | 49 +- src/Mod/FreeCADExchange/AutoRouting.py | 290 ++++--- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 180 ++-- src/Mod/FreeCADExchange/InitGui.py | 9 +- src/Mod/FreeCADExchange/RoutingNetwork.py | 810 ++++++++++++++++-- src/Mod/FreeCADExchange/WiringObjects.py | 14 +- tests/manual/freecad_auto_routing_smoke.py | 12 +- .../freecad_exchange_auto_routing_test.py | 291 ++++--- 9 files changed, 1323 insertions(+), 418 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index d7240e9..b16f9e9 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -1,6 +1,6 @@ -# FreeCAD 3D 自动布线设计方案 +# FreeCAD 3D 布线连接设计方案 -本文档描述 QET / LightWork3D 与 FreeCAD 协同中的 3D 自动布线设计。 +本文档描述 QET / LightWork3D 与 FreeCAD 协同中的 3D 布线连接设计。 当前版本目标不是完整复刻 EPLAN Pro Panel 或 SOLIDWORKS Electrical,而是先完成一个可用的最小闭环: @@ -17,7 +17,7 @@ ### 1.1 当前版本目标 -当前版本只要求完成“能够自动布线”: +当前版本只要求完成“能够生成布线连接”: 1. 能识别 FreeCAD 文档中的工程端子。 2. 能在一键布线前检查并绑定工程端子,把可匹配的 `local:*` 模板端子提升为真实 QET `terminal_uuid`。 @@ -37,7 +37,7 @@ 4. 不做完整线槽容量计算。 5. 不做线径、弯曲半径、线束层叠排列。 6. 不做强弱电隔离、EMC、屏蔽线等电气规则。 -7. 不保证达到 EPLAN / SOLIDWORKS Electrical 的完整工程级自动布线效果。 +7. 不保证达到 EPLAN / SOLIDWORKS Electrical 的完整工程级布线连接效果。 ### 1.3 数据库约束 @@ -71,7 +71,7 @@ terminal_uuid ## 2. 总体方案 -自动布线不直接在任意 3D 空间里找线,也不会把所有端子任意两两连接。它参考 EPLAN / SOLIDWORKS 的思路,先建立“可走路径网络”,再按 QET 导线任务中的起点端子和终点端子逐条求路。 +布线连接不直接在任意 3D 空间里找线,也不会把所有端子任意两两连接。它参考 EPLAN 的思路,先建立 routing path network,再按 QET 导线任务中的起点端子和终点端子逐条求路。 ```text 端子出线段 @@ -94,6 +94,8 @@ terminal_uuid 3. `TerminalAccess`:端子到路由网络的自动接入路径,只用于把工程端子接入线槽/布线面。 4. `AuxiliaryPath`:辅助路径,后续扩展使用。 5. `RoutingRange`:柜面/安装板等支撑面生成的辅助路由区域,成本较高,只用于过渡或没有线槽时兜底。 +6. `WireDuctOpenEnd`:线槽开口端横向路径,用于模拟 EPLAN 在线槽开口端生成的横向 routing path。 +7. `WiringCutOut`:穿线孔/过线孔路径载体,用于把线槽、安装面或柜体开孔处的可穿线路径接入网络。 普通机柜、设备外壳、实体边默认不是路由路径。 @@ -111,7 +113,7 @@ QetInstanceId = <3D instance_id> QetProjectUuid = ``` -端子空间位置来自 FreeCAD 文档。自动布线使用端子的全局坐标和方向。 +端子空间位置来自 FreeCAD 文档。生成布线连接时使用端子的全局坐标和方向。 模板实例生成的端子可能先处于本地状态: @@ -318,15 +320,17 @@ src/Mod/FreeCADExchange/InitGui.py 自动识别柜面/安装板时,系统只接受有支撑面语义且包围盒呈薄板形态的对象。识别出的对象标记为 `QetRoutingObstacleMode = "SupportSurface"`,用于避免沿支撑面走线时误报碰撞;普通机柜整体和设备外壳仍默认作为障碍物。 -生成布线布局空间时,系统按整份 FreeCAD 文档处理,不再要求用户选中某个面。它会先准备线槽/支撑面 carrier,再按端子 LCS 的全局坐标和出线方向生成短的 `TerminalAccess` 接入 carrier。这样正式自动布线更接近 EPLAN / SOLIDWORKS Electrical 的“一键准备布局空间并布线”逻辑。 +生成布线路径网络时,系统按整份 FreeCAD 文档处理,不再要求用户选中某个面。它会先准备线槽/支撑面 carrier,再按端子 LCS 的全局坐标和出线方向生成短的 `TerminalAccess` 接入 carrier。这样更接近 EPLAN 的“Generate routing path network -> Route”逻辑。 -### 5.3 自动布线功能 +生成线槽 carrier 时,系统除了 `WireDuct` 中心路径,还会在线槽两端生成 `WireDuctOpenEnd` 横向路径;对象名或标签包含 `Wiring Cut-Out`、`wire cutout`、`穿线孔`、`过线孔` 等语义时,会生成 `WiringCutOut` 穿线路径载体。 + +### 5.3 布线连接功能 已完成: 1. 检查/绑定工程端子,不生成导线。 -2. 两个选中端子之间自动布线。 -3. 根据导线任务批量自动布线。 +2. 两个选中端子之间生成布线连接。 +3. 根据导线任务批量生成布线连接。 4. 使用 Dijkstra 求路。 5. 支持在 carrier 交点、重叠段端点处自动换向。 6. 支持转弯惩罚。 @@ -335,22 +339,25 @@ src/Mod/FreeCADExchange/InitGui.py 9. 支持基于障碍 AABB 的路由图边级主动避障。 10. 无安全替代路径时保留碰撞检测和 `CollisionWarning` 状态。 11. 自动导线可见显示并保存到 FreeCAD 文档。 +12. 生成布线连接时保存 `QetRouteTrackJson`,记录实际经过的 `WireDuct` / `RoutingRange` / `TerminalAccess` / `WiringCutOut` carrier。 +13. 支持检查布线路径网络,诊断孤立网络、未接入端子和疑似线槽端点断点,并写入 `QETWiring_05_Diagnostics`。 ### 5.4 FreeCAD 面板 面板入口: ```text -QET模板 -> 3D自动布线 +QET模板 -> 3D布线连接 ``` -当前面板按 EPLAN / SOLIDWORKS Electrical 的使用习惯收敛为三步正式流程。测试性入口不再显示在面板上;`生成布线布局空间` 和 `自动布线` 都按整份 3D 装配执行,不再沿用“选中面/草图辅助路径”的测试流程。 +当前面板按 EPLAN 的使用习惯收敛为三步正式流程。测试性入口不再显示在面板上;`准备布线布局空间` 只识别并标记 layout space 里的线槽、安装面和工程端子,`生成布线路径网络` 再按整份 3D 装配生成 routing path network,`生成布线连接` 会在布线前更新同一套网络。 ```text -生成布线网络路径 -生成布线布局空间 -自动布线 -清除自动布线 +准备布线布局空间 +生成布线路径网络 +检查布线路径网络 +生成布线连接 +清除布线连接 清除走线路径 保存 ``` @@ -381,10 +388,13 @@ tests/python/freecad_exchange_auto_routing_test.py 14. 自动识别线槽模型生成中心路径,并避免把机柜模型误判为线槽。 15. 自动识别安装板/柜面生成 `RoutingRange`,并把支撑面标记为 `SupportSurface`。 16. 无线槽或线槽不完整时,可使用自动识别的支撑面辅助路径完成贴面布线。 -17. 面板流程已简化为“生成布线网络路径 -> 生成布线布局空间 -> 自动布线”。 -18. “生成布线网络路径”在有选择时从选中的线槽实体生成中心路径;没有选择时自动识别整份文档。 -19. “生成布线布局空间”始终按整份文档准备布局空间:自动识别线槽/支撑面,并生成端子接入 carrier。 -20. “自动布线”会先执行同一套布局空间准备逻辑,再按全部 QET 导线任务批量求路。 +17. 面板流程已简化为“准备布线布局空间 -> 生成布线路径网络 -> 生成布线连接”。 +18. “准备布线布局空间”始终按整份文档识别线槽、支撑面和工程端子,并标记障碍处理方式。 +19. “生成布线路径网络”按 EPLAN 的 Generate routing path network 语义生成 WireDuct、RoutingRange 和 TerminalAccess carrier;有选择时,选中线槽只作为额外识别提示,仍会扫描整份文档。 +20. “生成布线连接”会先更新同一套布线路径网络,再按全部 QET 导线任务批量求路。 +21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 +22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 +23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 已完成 FreeCAD smoke: @@ -400,26 +410,26 @@ tests/manual/freecad_auto_routing_smoke.py ```text 1. 打开 FreeCAD 工程 scene.FCStd -2. 进入 QET模板 -> 3D自动布线 -3. 清除自动布线 +2. 进入 QET模板 -> 3D布线连接 +3. 清除布线连接 4. 清除走线路径 -5. 可选:全选或选中线槽实体后点击“生成布线网络路径”;如果不选择,则使用整份文档自动识别 -6. 点击“生成布线网络路径” -7. 点击“生成布线布局空间” -8. 点击“自动布线” +5. 点击“准备布线布局空间” +6. 可选:选中无法自动识别的线槽实体 +7. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别 +8. 点击“生成布线连接” ``` 三个按钮的职责: ```text -生成布线网络路径:生成 WireDuct 中心线 carrier -生成布线布局空间:按整份装配准备线槽、支撑面和端子接入网络 -自动布线:先准备布局空间,再自动检查/绑定工程端子,按 QET 导线任务批量求路并生成 AutoSuggested 导线 +准备布线布局空间:识别并标记 layout space 里的线槽、支撑面、工程端子和障碍处理方式 +生成布线路径网络:按 EPLAN routing path network 逻辑生成 WireDuct、RoutingRange 和 TerminalAccess carrier +生成布线连接:先更新布线路径网络,再检查/绑定工程端子,按 QET 导线任务批量求路并生成 AutoSuggested 导线 ``` -如果模型名称/标签足够规范,可以不手动选择,直接执行三步;也可以只点击“自动布线”,系统会自动准备当前可识别的布线网络和布局空间。若线槽无法自动识别,则先选中线槽实体执行“生成布线网络路径”作为补充。 +如果模型名称/标签足够规范,可以不手动选择,直接执行三步;也可以只点击“生成布线连接”,系统会准备当前可识别的布线路径网络。若线槽无法自动识别,则先选中线槽实体执行“生成布线路径网络”作为补充。 -### 6.2 批量导线自动布线前提 +### 6.2 批量生成布线连接前提 1. QET 导出的 `2d_to_3d.json` 中包含 `wires[]`。 2. 每条导线包含: @@ -434,13 +444,13 @@ end_terminal_display ``` 3. FreeCAD 文档中存在对应 `QetTerminalUuid` 的工程端子,或存在可按设备和端子显示号匹配的 `local:*` 模板端子。 -4. 自动布线只按导线任务布线,不会把场景里所有端子任意两两相连。 +4. 布线连接只按导线任务生成,不会把场景里所有端子任意两两相连。 -注意:批量自动布线的依据是导线任务,不是“所有端子自动互连”。如果文档中只有端子而没有 `wires[]` 或 `QETWiring_01_Tasks`,系统不能判断哪些端子应该连接。 +注意:批量生成布线连接的依据是导线任务,不是“所有端子自动互连”。如果文档中只有端子而没有 `wires[]` 或 `QETWiring_01_Tasks`,系统不能判断哪些端子应该连接。 ## 7. 当前限制 -当前版本可完成自动布线原型,但仍有以下限制: +当前版本可完成布线连接原型,但仍有以下限制: 1. 线槽实体中心线生成基于包围盒长轴,不理解真实线槽开口、盖板和内部空间。 2. 多根线会沿同一路径生成,暂未做并行错位排列。 @@ -449,7 +459,7 @@ end_terminal_display 5. 未做强弱电分槽、线缆类型隔离。 6. 障碍检测基于 AABB,存在误报和漏报。 7. 辅助路由区域是网格近似,不等于专业软件的完整布线区域建模。 -8. 端子出线方向依赖端子 LCS 方向;如果模板端子方向不准,自动布线会受影响。 +8. 端子出线方向依赖端子 LCS 方向;如果模板端子方向不准,布线连接会受影响。 9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。 ## 8. 后续需要完成 @@ -531,20 +541,20 @@ PE 线优先路径 ## 9. 验收标准 -当前版本验收只看“能否自动布线”: +当前版本验收只看“能否生成布线连接”: 1. 文档中有至少两个真实工程端子。 2. 文档中有至少一条 `WireDuct` carrier,或有可作为低优先级路径的 `RoutingRange` 支撑面 carrier。 3. 执行“生成布线网络路径”后,能生成 `WireDuct` carrier。 4. 执行“生成布线布局空间”后,能生成或复用 `WireDuct` / `RoutingRange` carrier,并为工程端子生成 `TerminalAccess` 接入 carrier。 -5. 存在导线任务时执行“自动布线”,会先准备布局空间,再批量生成 `AutoSuggested` 导线。 +5. 存在导线任务时执行“生成布线连接”,会先准备布线路径网络,再批量生成 `AutoSuggested` 导线。 6. 生成导线在 `QETWiring_04_Routed` 下可见。 7. 没有路由网络时正式布线不生成长距离悬空线。 8. 没有导线任务时,批量布线明确提示缺少连接关系。 9. 有备选 carrier 时,明显障碍会被绕开,生成导线状态为 `Routed`。 10. 没有备选 carrier 时,明显碰撞状态为 `CollisionWarning`。 11. 两条相交或重叠的线槽中心路径能在交点/重叠端点处连通并自动拐弯。 -12. 自动识别出的安装板/柜面能生成低优先级 `RoutingRange`,并可被自动布线使用。 +12. 自动识别出的安装板/柜面能生成低优先级 `RoutingRange`,并可被布线连接使用。 13. 保存 FreeCAD 文档后,自动导线和路由网络仍保留。 ## 10. 开发验证命令 diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index 363f827..52cd607 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -9,7 +9,7 @@ - 导轨、线槽等柜内附件摆放 - 工程端子生成 - 手动布线与保存回写 -- 自动布线的基础准备 +- 布线连接的基础准备 本文档遵守当前第一版 2D/3D 协同约束: @@ -53,7 +53,7 @@ - 导入 FCStd 设备模板实例。 - 从模板端子生成工程端子。 - 打开 `3D手动布线` 面板。 -- 打开 `3D自动布线` 面板。 +- 打开 `3D布线连接` 面板。 - 保存并回写。 当前工具栏/菜单中常见命令: @@ -68,7 +68,7 @@ | `生成工程端子` | 把模板端子转换成当前工程里的可布线端子 | | `连接选中端子` | 连接两个已选工程端子 | | `3D手动布线` | 打开手动布线面板 | -| `3D自动布线` | 打开自动布线面板 | +| `3D布线连接` | 打开布线连接面板 | ### 1.3 `Draft` @@ -109,7 +109,7 @@ -> 放置设备 -> 为设备生成工程端子 -> 标记线槽/导轨/柜面 - -> 手动布线或自动布线 + -> 手动布线或布线连接 -> 保存并回写 ``` @@ -117,7 +117,7 @@ - `Assembly` 管“东西放哪儿”。 - `QET模板` 管“哪里能接线”。 -- `3D手动布线` / `3D自动布线` 管“线怎么走”。 +- `3D手动布线` / `3D布线连接` 管“线怎么走”。 - `scene.FCStd` 是 3D 状态真相源。 --- @@ -166,7 +166,7 @@ data/examples/qet_cabinet_assets/qet_wire_duct.FCStd data/examples/qet_cabinet_assets/qet_wire_duct.step ``` -线槽需要在工程里标记为“线槽”,这样自动布线或路径分析才能把它当作走线路径参考。 +线槽需要在工程里标记为“线槽”,这样布线连接或路径分析才能把它当作走线路径参考。 ### 3.4 有接线点的设备 @@ -390,7 +390,7 @@ Z = 1200 mm ### 8.3 标记柜面 -如果希望后续自动布线知道哪些面是柜内障碍或辅助区域: +如果希望后续布线连接知道哪些面是柜内障碍或辅助区域: 1. 选择机柜背板或安装板对象。 2. 在 `3D手动布线` 面板点击 `标记为柜面`。 @@ -587,14 +587,14 @@ QETExchangeDevices --- -## 12. 自动布线 +## 12. 布线连接 -自动布线适合在端子和走线网络准备好后使用。 +布线连接适合在端子和走线网络准备好后使用。 -### 12.1 打开自动布线面板 +### 12.1 打开布线连接面板 1. 切换到 `QET模板`。 -2. 点击 `3D自动布线`。 +2. 点击 `3D布线连接`。 常用按钮: @@ -604,15 +604,14 @@ QETExchangeDevices | `从线槽实体生成中心路径` | 从线槽实体生成可走线路径 | | `从线槽/草图创建路由路径` | 从选中线槽或草图生成路径 | | `从选中面创建辅助路由区域` | 生成辅助路由区域 | -| `测试布线选中两个端子` | 对两个选中端子做单条自动布线测试 | -| `按导线任务自动布线全部` | 根据 QET 导线任务批量布线 | -| `清除自动布线` | 删除自动生成导线 | +| `生成布线连接` | 根据 QET 导线任务批量生成布线连接 | +| `清除布线连接` | 删除生成的布线连接 | | `清除走线路径` | 删除路由载体 | | `保存` | 保存文档和回写结果 | -### 12.2 自动布线前置条件 +### 12.2 布线连接前置条件 -自动布线前建议先满足: +布线连接前建议先满足: 1. 设备已经摆放到位。 2. 工程端子已经生成。 @@ -623,17 +622,17 @@ QETExchangeDevices ### 12.3 生成线槽中心路径 1. 选择线槽对象。 -2. 打开 `3D自动布线` 面板。 +2. 打开 `3D布线连接` 面板。 3. 点击 `从线槽实体生成中心路径`。 4. 点击 `扫描端子/网络`。 如果扫描结果显示有 carrier / segment / node,说明走线网络已建立。 -### 12.4 批量自动布线 +### 12.4 批量生成布线连接 1. 确认 QET 已导入导线任务。 2. 点击 `扫描端子/网络`。 -3. 点击 `按导线任务自动布线全部`。 +3. 点击 `生成布线连接`。 4. 查看状态中的 routed、collision_warnings、missing_terminals。 5. 若有 missing terminals,说明某些 2D 端子没有对应工程端子。 6. 保存。 @@ -660,7 +659,7 @@ scene.FCStd 保存并回写 ``` -或在自动布线面板点击: +或在布线连接面板点击: ```text 保存 @@ -735,7 +734,7 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 2. 选择其中的工程端子。 3. 再执行 `设为起点` / `设为终点并生成`。 -### 15.3 为什么自动布线找不到路径? +### 15.3 为什么布线连接找不到路径? 常见原因: @@ -747,10 +746,10 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 处理: 1. 选择线槽,点击 `标记为线槽`。 -2. 打开 `3D自动布线`。 +2. 打开 `3D布线连接`。 3. 点击 `从线槽实体生成中心路径`。 4. 点击 `扫描端子/网络`。 -5. 再尝试自动布线。 +5. 再尝试生成布线连接。 ### 15.4 为什么保存后 QET 看不到 3D 位姿? @@ -786,7 +785,7 @@ QET 侧只依赖最小绑定字段找到对应设备和端子。 5. 端子排优先用单片端子复制,不要每次重建。 6. 每完成一段装配就保存一次 `scene.FCStd`。 7. 布线前先生成工程端子。 -8. 自动布线前先建立线槽中心路径。 +8. 生成布线连接前先建立布线路径网络。 9. 不要手动改工程绑定 UUID。 10. 不要依赖旧 3D 场景表保存位姿。 @@ -794,4 +793,4 @@ QET 侧只依赖最小绑定字段找到对应设备和端子。 ## 17. 一句话总结 -机柜装配用 `Assembly` 把设备放准;端子语义用 `QET模板` 写进 FCStd 模板;工程中点击 `生成工程端子` 后,再用 `3D手动布线` 或 `3D自动布线` 连接工程端子;最终保存的是 `scene.FCStd`,它是 3D 装配和布线状态的真相源。 +机柜装配用 `Assembly` 把设备放准;端子语义用 `QET模板` 写进 FCStd 模板;工程中点击 `生成工程端子` 后,再用 `3D手动布线` 或 `3D布线连接` 连接工程端子;最终保存的是 `scene.FCStd`,它是 3D 装配和布线状态的真相源。 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 6dff20c..144a272 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1,4 +1,4 @@ -# FreeCADExchange 3D automatic wiring. +# FreeCADExchange 3D routing connections. # # 第一版不碰 C++,也不把 3D 走线结果写进数据库。 # 它只读取 FreeCAD 文档里的端子、走线网络和几何障碍, @@ -23,9 +23,6 @@ import WiringObjects DEFAULT_OPTIONS = { # 端子出来先走一小段,避免导线贴着设备外壳起步。 "terminal_exit_length": 20.0, - # 没有线槽网络时,退回到这个方向抬高/偏移后做正交折线。 - "clearance_axis": "z", - "clearance": 80.0, "lane_axis": "y", "lane_spacing": 10.0, # 线槽网络相关参数。 @@ -35,15 +32,15 @@ DEFAULT_OPTIONS = { # EPLAN/SOLIDWORKS 风格:线槽/路由路径最优先,辅助面域只作为过渡/兜底区域。 "carrier_kind_cost_factors": { "WireDuct": 1.0, + "WireDuctOpenEnd": 1.0, + "WiringCutOut": 1.0, "RoutingPath": 1.0, "UserPath": 1.0, "AuxiliaryPath": 2.0, "TerminalAccess": 2.0, "RoutingRange": 8.0, - "SurfaceGrid": 8.0, }, - # 默认不再生成长距离悬空 fallback;主干必须走 carrier/贴面网络。 - "allow_floating_fallback": False, + # 主干必须走 carrier/贴面网络;没有布线路径网络时直接失败。 # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 "obstacle_clearance": 5.0, # 端子出线/入线段通常会贴近端子塑壳或设备外壳,不作为主路径碰撞判定依据。 @@ -479,8 +476,8 @@ def _bind_wire_task_terminals(doc, payload): def _wire_object_name(start_terminal, end_terminal, wire_uuid=""): if wire_uuid: - return "QETAutoWire_{0}".format(TerminalObjects.safe_token(wire_uuid)) - return "QETAutoWire_{0}_{1}".format( + return "QETRoutedConnection_{0}".format(TerminalObjects.safe_token(wire_uuid)) + return "QETRoutedConnection_{0}_{1}".format( TerminalObjects.safe_token(getattr(start_terminal, "QetTerminalUuid", "")), TerminalObjects.safe_token(getattr(end_terminal, "QetTerminalUuid", "")), ) @@ -540,13 +537,13 @@ def _create_wire_geometry(doc, name, points): def _set_points(obj, points): try: if "Points" not in getattr(obj, "PropertiesList", []): - obj.addProperty("App::PropertyVectorList", "Points", "QET Wiring", "Auto route points") + obj.addProperty("App::PropertyVectorList", "Points", "QET Wiring", "Route points") obj.Points = list(points) except Exception: pass -def _set_string(obj, name, value, description="Auto-routing property"): +def _set_string(obj, name, value, description="Routing connection property"): TerminalObjects.ensure_string_property(obj, name, "QET Routing", description, value) @@ -561,22 +558,23 @@ def _route_payload(route_data, collisions, wire_style_id=""): "collision_count": len(collisions), "collisions": collisions, "network": route_data.get("network", {}), + "route_track": route_data.get("route_track", {}), } -def _set_auto_metadata(wire, route_data, collisions, wire_style_id=""): +def _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id=""): length_mm = _route_length(route_data.get("points", [])) _set_string( wire, - "QetAutoRouteAlgorithm", + "QetRouteAlgorithm", route_data.get("algorithm", ""), - "Auto-routing algorithm used for this wire", + "Routing connection algorithm used for this wire", ) _set_string( wire, - "QetAutoRouteLengthMm", + "QetRouteLengthMm", "{0:.3f}".format(length_mm), - "Auto route length in millimeters", + "Routing connection length in millimeters", ) _set_string( wire, @@ -586,56 +584,24 @@ def _set_auto_metadata(wire, route_data, collisions, wire_style_id=""): ) _set_string( wire, - "QetAutoRouteDiagnosticsJson", + "QetRouteDiagnosticsJson", json.dumps(_route_payload(route_data, collisions, wire_style_id=wire_style_id), ensure_ascii=False), - "Auto-routing diagnostics", + "Routing connection diagnostics", ) if route_data.get("network"): _set_string( wire, - "QetAutoRouteNetworkJson", + "QetRouteNetworkJson", json.dumps(route_data.get("network", {}), ensure_ascii=False), "Route network metadata used by this wire", ) - - -def build_orthogonal_route(start_terminal, end_terminal, route_index=0, options=None): - opts = _merged_options(options) - start_origin = _terminal_origin(start_terminal) - end_origin = _terminal_origin(end_terminal) - exit_length = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) - start_exit = _offset(start_origin, _terminal_direction(start_terminal), exit_length) - end_exit = _offset(end_origin, _terminal_direction(end_terminal), exit_length) - - clearance_axis = (opts.get("clearance_axis") or "z").lower() - if clearance_axis not in {"x", "y", "z"}: - clearance_axis = "z" - lane = _lane_payload(route_index, opts) - - clearance_value = max( - _axis_value(start_exit, clearance_axis), - _axis_value(end_exit, clearance_axis), - ) + float(opts.get("clearance", 0.0) or 0.0) - - lane_point = _with_axis(start_exit, clearance_axis, clearance_value) - lane_point = _with_axis(lane_point, lane["axis"], _axis_value(lane_point, lane["axis"]) + lane["offset_mm"]) - end_lane = _with_axis(end_exit, clearance_axis, clearance_value) - end_lane = _with_axis(end_lane, lane["axis"], _axis_value(end_lane, lane["axis"]) + lane["offset_mm"]) - - points = [] - _append_unique(points, start_origin) - _append_unique(points, start_exit) - _append_orthogonal(points, lane_point, preferred_axis=clearance_axis) - _append_orthogonal(points, end_lane) - _append_orthogonal(points, end_exit, preferred_axis=clearance_axis) - _append_unique(points, end_origin) - - return { - "algorithm": "orthogonal-v1", - "points": points, - "network": {}, - "lane": lane, - } + if route_data.get("route_track"): + _set_string( + wire, + "QetRouteTrackJson", + json.dumps(route_data.get("route_track", {}), ensure_ascii=False), + "Routing carriers passed through by this wire", + ) def build_network_route(start_terminal, end_terminal, route_index=0, options=None, doc=None): @@ -669,13 +635,14 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non ): return None - path_keys = RoutingNetwork.shortest_path( + path_result = RoutingNetwork.shortest_path_with_carriers( network, start_key, end_key, bend_penalty=float(opts.get("bend_penalty", 0.0) or 0.0), kind_cost_factors=opts.get("carrier_kind_cost_factors", {}), ) + path_keys = path_result.get("path", []) if isinstance(path_result, dict) else [] if not path_keys: return None @@ -706,6 +673,7 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "exit_distance": float(end_distance or 0.0), "obstacle_aware": bool(obstacle_aware), }, + "route_track": path_result, "lane": lane, } @@ -939,10 +907,10 @@ def _detach_object_from_groups(doc, obj): pass -def _remove_existing_auto_routes(doc, start_uuid, end_uuid, wire_uuid=""): +def _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=""): removed = 0 for obj in list(WiringObjects.iter_routed_wire_objects(doc)): - if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested": + if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": continue if wire_uuid: if (getattr(obj, "QetWireUuid", "") or "").strip() != wire_uuid: @@ -1010,7 +978,7 @@ def _style_wire(wire, collision_count=0): pass -def route_between_terminals( +def route_eplan_connection_between_terminals( doc, start_terminal, end_terminal, @@ -1039,10 +1007,10 @@ def route_between_terminals( end_uuid = (getattr(end_terminal, "QetTerminalUuid", "") or "").strip() project_uuid = _project_uuid(doc, start_terminal, end_terminal) if not project_uuid: - raise AutoRoutingError("Project UUID is required for auto-routing.") + raise AutoRoutingError("Project UUID is required for routing connections.") if opts.get("replace_existing", True): - _remove_existing_auto_routes(doc, start_uuid, end_uuid, wire_uuid=wire_uuid) + _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=wire_uuid) route_data = build_network_route( start_terminal, @@ -1052,20 +1020,13 @@ def route_between_terminals( doc=doc, ) if route_data is None: - if not opts.get("allow_floating_fallback", False): - raise AutoRoutingError( - "没有可用的线槽/路由路径网络;请先自动识别线槽生成路径,或选择线槽实体生成中心路径。" - ) - route_data = build_orthogonal_route( - start_terminal, - end_terminal, - route_index=route_index, - options=opts, + raise AutoRoutingError( + "没有可用的布线路径网络;请先生成布线布局空间和布线路径网络。" ) points = route_data.get("points", []) if len(points) < 2: - raise AutoRoutingError("Auto-routing produced fewer than two points.") + raise AutoRoutingError("Routing connection produced fewer than two points.") obstacles = collect_obstacles(doc, exclude=[start_terminal, end_terminal], options=opts) ignored_collision_segments = set() @@ -1076,7 +1037,7 @@ def route_between_terminals( wire_name = _unique_name(doc, _wire_object_name(start_terminal, end_terminal, wire_uuid)) wire = _create_wire_geometry(doc, wire_name, points) - wire.Label = wire_label or wire_mark or wire_uuid or "QET Auto Wire" + wire.Label = wire_label or wire_mark or wire_uuid or "QET Routed Connection" WiringObjects.set_routed_wire_semantics( wire, project_uuid, @@ -1086,15 +1047,15 @@ def route_between_terminals( end_uuid, (getattr(start_terminal, "QetInstanceId", "") or "").strip(), (getattr(end_terminal, "QetInstanceId", "") or "").strip(), - route_type="AutoSuggested", + route_type="RoutedConnection", route_status=status, - route_mode="Auto", + route_mode="EplanRoute", net_uuid=net_uuid, group_uuid=group_uuid, wire_mark=wire_mark, wire_mark_is_manual=wire_mark_is_manual, ) - _set_auto_metadata(wire, route_data, collisions, wire_style_id=effective_wire_style_id) + _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id=effective_wire_style_id) routed_group = WiringObjects.ensure_routed_group(doc, project_uuid) if wire not in getattr(routed_group, "Group", []): @@ -1118,6 +1079,7 @@ def route_between_terminals( "route_status": status, "algorithm": route_data.get("algorithm", ""), "network": route_data.get("network", {}), + "route_track": route_data.get("route_track", {}), "points": points, "lane": route_data.get("lane", {}), "length_mm": _route_length(points), @@ -1178,7 +1140,7 @@ def format_terminal_binding_report(report): return message -def route_all_from_payload(doc, payload, options=None, prepared_layout=None): +def route_eplan_connections_from_payload(doc, payload, options=None, prepared_layout=None): if doc is None: raise AutoRoutingError("No FreeCAD document is available.") if not isinstance(payload, dict): @@ -1245,7 +1207,7 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): ) continue try: - result = route_between_terminals( + result = route_eplan_connection_between_terminals( doc, start_terminal, end_terminal, @@ -1293,17 +1255,18 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): "length_mm": route_length, "lane": result.get("lane", {}), "network": result.get("network", {}), + "route_track": result.get("route_track", {}), "collision_count": result["collision_count"], "collision_samples": route_collision_samples, } ) report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) - _write_auto_route_batch_diagnostic(doc, report) + _write_routing_connection_batch_diagnostic(doc, report) return report -def format_route_all_report(report): - message = "批量自动布线完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( +def format_eplan_connection_route_report(report): + message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), report.get("collision_warnings", 0), report.get("skipped_missing_terminal", 0), @@ -1317,7 +1280,7 @@ def format_route_all_report(report): ) total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) if total_length_mm > 0.0: - message += "\n自动布线总长度:{0:.1f} mm。".format(total_length_mm) + message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) errors = report.get("errors", []) or [] if errors: message += "\n首个错误:{0}".format(str(errors[0])) @@ -1360,11 +1323,11 @@ def format_route_all_report(report): return message -def _clear_auto_route_batch_diagnostics(doc): +def _clear_routing_connection_batch_diagnostics(doc): group = WiringObjects.ensure_diagnostic_group(doc, _project_uuid(doc)) removed = 0 for obj in list(getattr(group, "Group", []) or []): - if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "AutoRouteBatch": + if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "RoutingConnectionBatch": continue try: group.removeObject(obj) @@ -1386,12 +1349,12 @@ def _clear_auto_route_batch_diagnostics(doc): return removed -def _write_auto_route_batch_diagnostic(doc, report): +def _write_routing_connection_batch_diagnostic(doc, report): if doc is None or not isinstance(report, dict): return None project_uuid = _project_uuid(doc) group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) - _clear_auto_route_batch_diagnostics(doc) + _clear_routing_connection_batch_diagnostics(doc) if ( report.get("total_wires", 0) <= 0 and not report.get("routes") @@ -1400,14 +1363,14 @@ def _write_auto_route_batch_diagnostic(doc, report): and report.get("collision_warnings", 0) <= 0 ): return None - diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETAutoRouteDiagnostic")) - diagnostic.Label = "QET Auto Route Diagnostic" - _set_string(diagnostic, "QetDiagnosticKind", "AutoRouteBatch", "QET diagnostic kind") + diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETRoutingConnectionDiagnostic")) + diagnostic.Label = "QET Routing Connection Diagnostic" + _set_string(diagnostic, "QetDiagnosticKind", "RoutingConnectionBatch", "QET diagnostic kind") _set_string( diagnostic, "QetDiagnosticJson", json.dumps(report, ensure_ascii=False), - "QET auto-routing batch diagnostic payload", + "QET routing connection batch diagnostic payload", ) group.addObject(diagnostic) return diagnostic @@ -1456,18 +1419,54 @@ def bind_wire_task_terminals_from_tasks(doc): return bind_wire_task_terminals_from_payload(doc, _wire_tasks_payload(doc)) -def route_all_tasks(doc, options=None, prepared_layout=None): +def route_eplan_connection_tasks(doc, options=None, prepared_layout=None): payload = _wire_tasks_payload(doc) - return route_all_from_payload(doc, payload, options=options, prepared_layout=prepared_layout) + return route_eplan_connections_from_payload(doc, payload, options=options, prepared_layout=prepared_layout) -def prepare_eplan_style_layout(doc, project_uuid="", options=None): - """Prepare the whole document for production auto-routing. +def prepare_eplan_layout_space(doc, project_uuid=""): + """Prepare the FreeCAD document as an EPLAN-style layout space. - EPLAN/SW 的操作语义是“对布线布局空间执行布线”,不是要求用户先点面、 - 画草图或手工补每个端子的接入线。这里统一生成:线槽中心路径、柜内 - 可布线面,以及端子到路由网络的自动接入 carrier。 + This step marks layout-space source objects and wiring buckets, but does + not generate the routing path network. In EPLAN terms, the layout space is + the 3D installation context in which the network is later generated. """ + if doc is None: + raise AutoRoutingError("No FreeCAD document is available.") + target_project_uuid = (project_uuid or "").strip() or _project_uuid(doc) + if not target_project_uuid: + try: + target_project_uuid = (getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "") or "").strip() + except Exception: + target_project_uuid = "" + return RoutingNetwork.prepare_layout_space_sources_from_document( + doc, + project_uuid=target_project_uuid, + ) + + +def generate_eplan_routing_path_network(doc, project_uuid="", options=None, selection_ex=None): + """Generate the routing path network for the current layout space.""" + if doc is None: + raise AutoRoutingError("No FreeCAD document is available.") + opts = _merged_options(options) + target_project_uuid = (project_uuid or "").strip() or _project_uuid(doc) + if not target_project_uuid: + try: + target_project_uuid = (getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "") or "").strip() + except Exception: + target_project_uuid = "" + return RoutingNetwork.create_routing_path_network_from_document( + doc, + project_uuid=target_project_uuid, + selection_ex=selection_ex, + 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), + ) + + +def check_eplan_routing_path_network(doc, project_uuid="", options=None): + """Write and return routing path network diagnostics for the layout space.""" if doc is None: raise AutoRoutingError("No FreeCAD document is available.") opts = _merged_options(options) @@ -1477,22 +1476,84 @@ def prepare_eplan_style_layout(doc, project_uuid="", options=None): target_project_uuid = (getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "") or "").strip() except Exception: target_project_uuid = "" - return RoutingNetwork.create_layout_space_from_document( + result = RoutingNetwork.write_routing_path_network_diagnostic( doc, project_uuid=target_project_uuid, 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), ) + diagnostic = result.get("diagnostic", {}) if isinstance(result, dict) else {} + return { + "diagnostic": diagnostic, + "diagnostic_object": result.get("diagnostic_object") if isinstance(result, dict) else None, + "ok": bool(diagnostic.get("ok", False)) if isinstance(diagnostic, dict) else False, + "issue_count": len(diagnostic.get("issues", []) or []) if isinstance(diagnostic, dict) else 0, + } + + +def update_eplan_routing_path_network(doc, project_uuid="", options=None, selection_ex=None): + """Update the routing path network before EPLAN-style Route.""" + return generate_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=options, + selection_ex=selection_ex, + ) +def route_eplan_connections( + doc, + payload=None, + options=None, + project_uuid="", + selection_ex=None, + update_network=True, +): + """Route QET wire tasks through the EPLAN-style routing path network.""" + if doc is None: + raise AutoRoutingError("No FreeCAD document is available.") + + prepared_network = None + if update_network: + prepared_network = update_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=options, + selection_ex=selection_ex, + ) + + target_payload = payload + if target_payload is None: + target_payload = getattr(App, "_qet_exchange_payload", None) + + if isinstance(target_payload, dict) and target_payload.get("wires"): + report = route_eplan_connections_from_payload( + doc, + target_payload, + options=options, + prepared_layout=prepared_network, + ) + else: + report = route_eplan_connection_tasks( + doc, + options=options, + prepared_layout=prepared_network, + ) + + report["routing_method"] = "eplan-route-v1" + report["routing_path_network_updated"] = bool(update_network) + if isinstance(prepared_network, dict): + report["routing_path_network"] = prepared_network + return report + def wire_task_count(doc): return len(_iter_wire_tasks(doc)) -def clear_auto_routes(doc): +def clear_routing_connections(doc): removed = 0 for obj in list(WiringObjects.iter_routed_wire_objects(doc)): - if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested": + if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": continue try: _detach_object_from_groups(doc, obj) @@ -1521,11 +1582,11 @@ def _console_error(message): pass -class CommandAutoRouteAll: +class CommandRouteEplanConnections: def GetResources(self): return { - "MenuText": "一键自动布线(全部导线)", - "ToolTip": "自动识别线槽/安装板并生成全部 3D 布线路径", + "MenuText": "生成布线连接(全部导线)", + "ToolTip": "按布线路径网络生成全部 3D 布线连接", } def IsActive(self): @@ -1534,19 +1595,18 @@ class CommandAutoRouteAll: def Activated(self): doc = getattr(App, "ActiveDocument", None) try: - prepared_layout = prepare_eplan_style_layout(doc) - payload = getattr(App, "_qet_exchange_payload", None) - if isinstance(payload, dict) and payload.get("wires"): - report = route_all_from_payload(doc, payload, prepared_layout=prepared_layout) - else: - report = route_all_tasks(doc, prepared_layout=prepared_layout) + report = route_eplan_connections( + doc, + payload=payload if isinstance(payload, dict) and payload.get("wires") else None, + update_network=True, + ) if report.get("total_wires", 0) <= 0: - _console_error("没有导线任务。一键自动布线需要 QET wires[] 或 QETWiring_01_Tasks。") + _console_error("没有导线任务。生成布线连接需要 QET wires[] 或 QETWiring_01_Tasks。") return - _console_message(format_route_all_report(report)) + _console_message(format_eplan_connection_route_report(report)) except Exception as exc: - _console_error("批量自动布线失败:{0}".format(exc)) + _console_error("批量生成布线连接失败:{0}".format(exc)) _COMMANDS_REGISTERED = False @@ -1558,7 +1618,7 @@ def register_commands(): return if Gui is None or not hasattr(Gui, "addCommand"): return - Gui.addCommand("QET_Exchange_AutoRouteAll", CommandAutoRouteAll()) + Gui.addCommand("QET_Exchange_RouteEplanConnections", CommandRouteEplanConnections()) _COMMANDS_REGISTERED = True diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 3824124..fb969cc 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -1,9 +1,9 @@ -# FreeCADExchange GUI panel for 3D automatic wiring. +# FreeCADExchange GUI panel for 3D routing connections. # -# EPLAN-style simplified workflow: -# 1. "生成布线网络路径" - generate wire-duct centerline carriers -# 2. "生成布线布局空间" - prepare surfaces and terminal access routes for the whole assembly -# 3. "自动布线" - prepare the layout again and route all QET wire tasks +# EPLAN-style workflow: +# 1. "准备布线布局空间" - mark the FreeCAD document as the 3D layout space +# 2. "生成布线路径网络" - generate the full routing path network +# 3. "生成布线连接" - update the network and route all QET wire tasks import FreeCAD as App @@ -33,7 +33,7 @@ except Exception: ExchangeWriteBack = None -COMMAND_NAME = "QET_Exchange_OpenAutoRoutingPanel" +COMMAND_NAME = "QET_Exchange_OpenRoutingConnectionPanel" class AutoRoutingPanelError(RuntimeError): @@ -101,54 +101,49 @@ class AutoRoutingController: ) def generate_routing_paths(self): - """Generate wire-duct routing paths from the current selection or whole document.""" + """Generate the full routing path network from the layout space.""" doc = _active_document() project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() selection_ex = _selection_ex() source_mode = "selection" if selection_ex else "document" - if selection_ex: - wire_ducts = RoutingNetwork.create_wire_duct_carriers_from_selection( - doc, - selection_ex, - project_uuid=project_uuid, - ) - else: - wire_ducts = RoutingNetwork.create_wire_duct_carriers_from_document( - doc, - project_uuid=project_uuid, - ) - self.last_report = { - "wire_duct_carriers": len(wire_ducts), - "source_mode": source_mode, - } + self.last_report = AutoRouting.generate_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + selection_ex=selection_ex, + ) + self.last_report["source_mode"] = source_mode + return self.last_report + + def check_routing_path_network(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + self.last_report = AutoRouting.check_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + ) return self.last_report def generate_layout_space(self): - """Prepare the whole document as an EPLAN-style routing layout space.""" + """Prepare the whole document as an EPLAN-style layout space.""" doc = _active_document() project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() - self.last_report = AutoRouting.prepare_eplan_style_layout( + self.last_report = AutoRouting.prepare_eplan_layout_space( doc, project_uuid=project_uuid, ) self.last_report["source_mode"] = "document" return self.last_report - def route_all(self): + def route_eplan_connections(self): doc = _active_document() project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() - # EPLAN-style one-click routing: prepare the whole layout space first, then - # solve every QET wire task. The user should not need to select faces or draw - # Draft/Sketch helper paths for normal production routing. - prepared_layout = AutoRouting.prepare_eplan_style_layout( + payload = getattr(App, "_qet_exchange_payload", None) + report = AutoRouting.route_eplan_connections( doc, + payload=payload if isinstance(payload, dict) and payload.get("wires") else None, project_uuid=project_uuid, + update_network=True, ) - payload = getattr(App, "_qet_exchange_payload", None) - if isinstance(payload, dict) and payload.get("wires"): - report = AutoRouting.route_all_from_payload(doc, payload, prepared_layout=prepared_layout) - else: - report = AutoRouting.route_all_tasks(doc, prepared_layout=prepared_layout) if report.get("total_wires", 0) <= 0: raise AutoRoutingPanelError( "没有导线任务。请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中存在导线任务。" @@ -156,9 +151,9 @@ class AutoRoutingController: self.last_report = report return report - def clear_auto_routes(self): + def clear_routing_connections(self): doc = _active_document() - removed = AutoRouting.clear_auto_routes(doc) + removed = AutoRouting.clear_routing_connections(doc) self.last_report = {"removed": removed} return removed @@ -184,26 +179,31 @@ class AutoRoutingTaskPanel: raise AutoRoutingPanelError("Qt widgets are not available.") self.controller = controller or AutoRoutingController() self.form = QtWidgets.QWidget() - self.form.setWindowTitle("3D 自动布线") + self.form.setWindowTitle("3D 布线连接") layout = QtWidgets.QVBoxLayout(self.form) - self.generate_paths_button = QtWidgets.QPushButton("生成布线网络路径") + self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间") + self.generate_layout_button.setToolTip( + "按 EPLAN 布局空间语义识别线槽、安装面、工程端子和障碍处理方式,不生成导线。" + ) + + self.generate_paths_button = QtWidgets.QPushButton("生成布线路径网络") self.generate_paths_button.setToolTip( - "优先从当前选择生成线槽中心路径;未选择时自动识别整份文档里的线槽。" + "按 EPLAN 逻辑从布局空间生成完整 routing path network:线槽、布线区域和端子接入。" ) - self.generate_layout_button = QtWidgets.QPushButton("生成布线布局空间") - self.generate_layout_button.setToolTip( - "按整份 3D 装配生成布线布局空间:识别线槽/安装面,并把工程端子自动接入路由网络。" + self.check_paths_button = QtWidgets.QPushButton("检查布线路径网络") + self.check_paths_button.setToolTip( + "检查 routing path network 的断点、孤立网络和未接入端子,并写入诊断对象。" ) - self.route_all_button = QtWidgets.QPushButton("自动布线") - self.route_all_button.setToolTip( - "一键准备布线网络和布局空间,并按全部 QET 导线任务生成 3D 自动布线。" + self.route_connections_button = QtWidgets.QPushButton("生成布线连接") + self.route_connections_button.setToolTip( + "自动更新布线路径网络,并按全部 QET 导线任务生成 3D 布线连接。" ) - self.clear_routes_button = QtWidgets.QPushButton("清除自动布线") + self.clear_routes_button = QtWidgets.QPushButton("清除布线连接") self.clear_carriers_button = QtWidgets.QPushButton("清除走线路径") self.save_button = QtWidgets.QPushButton("保存") @@ -211,9 +211,10 @@ class AutoRoutingTaskPanel: self.status_label.setWordWrap(True) for widget in ( - self.generate_paths_button, self.generate_layout_button, - self.route_all_button, + self.generate_paths_button, + self.check_paths_button, + self.route_connections_button, self.clear_routes_button, self.clear_carriers_button, self.save_button, @@ -223,9 +224,10 @@ class AutoRoutingTaskPanel: layout.addWidget(self.status_label) self.generate_paths_button.clicked.connect(self.generate_routing_paths) + self.check_paths_button.clicked.connect(self.check_routing_path_network) self.generate_layout_button.clicked.connect(self.generate_layout_space) - self.route_all_button.clicked.connect(self.route_all) - self.clear_routes_button.clicked.connect(self.clear_auto_routes) + self.route_connections_button.clicked.connect(self.route_eplan_connections) + self.clear_routes_button.clicked.connect(self.clear_routing_connections) self.clear_carriers_button.clicked.connect(self.clear_route_carriers) self.save_button.clicked.connect(self.save) @@ -249,15 +251,50 @@ class AutoRoutingTaskPanel: try: result = self.controller.generate_routing_paths() wire_ducts = result.get("wire_duct_carriers", 0) - if wire_ducts == 0: + surfaces = result.get("surface_carriers", 0) + terminal_access = result.get("terminal_access_carriers", 0) + network = result.get("network", {}) if isinstance(result.get("network", {}), dict) else {} + if network.get("segments", 0) == 0: self._set_status( - "未生成布线网络路径。可先全选或选中线槽实体,再执行本按钮。" + "未生成可用布线路径网络。可选中线槽实体,或确认安装板/背板能作为布线区域。" + self.controller.summary() ) return self._set_status( - "已生成布线网络路径:线槽中心路径 {0} 条。{1}".format( - wire_ducts, self.controller.summary() + "已生成布线路径网络:线槽路径 {0} 条,布线区域 {1} 条,端子接入 {2} 条,网络段 {3} 条。{4}".format( + wire_ducts, + surfaces, + terminal_access, + network.get("segments", 0), + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def check_routing_path_network(self): + try: + result = self.controller.check_routing_path_network() + diagnostic = result.get("diagnostic", {}) if isinstance(result.get("diagnostic", {}), dict) else {} + issues = diagnostic.get("issues", []) or [] + summary = diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {} + if not issues: + self._set_status( + "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。{3}".format( + summary.get("carriers", 0), + summary.get("segments", 0), + summary.get("nodes", 0), + self.controller.summary(), + ) + ) + return + first_issue = issues[0] + self._set_status( + "布线路径网络检查发现 {0} 类问题:{1} ({2})。{3}".format( + len(issues), + first_issue.get("code", ""), + first_issue.get("count", 0), + self.controller.summary(), ) ) except Exception as exc: @@ -266,38 +303,37 @@ class AutoRoutingTaskPanel: def generate_layout_space(self): try: result = self.controller.generate_layout_space() - wire_ducts = result.get("wire_duct_carriers", 0) - surfaces = result.get("surface_carriers", 0) - terminal_access = result.get("terminal_access_carriers", 0) - network = result.get("network", {}) if isinstance(result.get("network", {}), dict) else {} - if network.get("segments", 0) == 0: + wire_duct_sources = result.get("wire_duct_sources", 0) + support_sources = result.get("support_surface_sources", 0) + terminals = result.get("routable_terminals", 0) + if wire_duct_sources == 0 and support_sources == 0: self._set_status( - "未生成可用布线布局空间。请确认 3D 装配里有可识别的线槽、安装板、背板或已标记路由路径。" + "未识别到可布线布局空间源。请确认 3D 装配里有可识别的线槽、安装板或背板。" + self.controller.summary() ) return self._set_status( - "已生成布线布局空间:线槽路径 {0} 条,布线面 {1} 条,端子接入 {2} 条。{3}".format( - wire_ducts, - surfaces, - terminal_access, + "已准备布线布局空间:线槽源 {0} 个,布线区域源 {1} 个,工程端子 {2} 个。{3}".format( + wire_duct_sources, + support_sources, + terminals, self.controller.summary(), ) ) except Exception as exc: self._set_error(str(exc)) - def route_all(self): + def route_eplan_connections(self): try: - report = self.controller.route_all() - self._set_status(AutoRouting.format_route_all_report(report)) + report = self.controller.route_eplan_connections() + self._set_status(AutoRouting.format_eplan_connection_route_report(report)) except Exception as exc: self._set_error(str(exc)) - def clear_auto_routes(self): + def clear_routing_connections(self): try: - removed = self.controller.clear_auto_routes() - self._set_status("已清除自动布线:{0} 条。".format(removed)) + removed = self.controller.clear_routing_connections() + self._set_status("已清除布线连接:{0} 条。".format(removed)) except Exception as exc: self._set_error(str(exc)) @@ -326,8 +362,8 @@ class AutoRoutingTaskPanel: class CommandOpenAutoRoutingPanel: def GetResources(self): return { - "MenuText": "3D自动布线", - "ToolTip": "打开 3D 自动布线面板", + "MenuText": "3D布线连接", + "ToolTip": "打开 3D 布线连接面板", } def IsActive(self): diff --git a/src/Mod/FreeCADExchange/InitGui.py b/src/Mod/FreeCADExchange/InitGui.py index 9294258..0173a0b 100644 --- a/src/Mod/FreeCADExchange/InitGui.py +++ b/src/Mod/FreeCADExchange/InitGui.py @@ -16,9 +16,8 @@ COMMANDS = [ "QET_Template_CreateEngineeringTerminals", "QET_Exchange_CreateManualWire", "QET_Exchange_OpenManualWiringPanel", - "QET_Exchange_AutoRouteSelected", - "QET_Exchange_AutoRouteAll", - "QET_Exchange_OpenAutoRoutingPanel", + "QET_Exchange_RouteEplanConnections", + "QET_Exchange_OpenRoutingConnectionPanel", "QET_Exchange_HideStaleObjects", "QET_Exchange_ShowStaleObjects", "QET_Exchange_SummarizeStaleObjects", @@ -103,7 +102,7 @@ def _register_exchange_commands( auto_routing.register_commands() except Exception: append_init_log( - "InitGui failed to register auto-routing commands:\n{0}".format( + "InitGui failed to register routing connection commands:\n{0}".format( traceback_module.format_exc() ) ) @@ -113,7 +112,7 @@ def _register_exchange_commands( auto_routing_panel.register_commands() except Exception: append_init_log( - "InitGui failed to register auto-routing panel command:\n{0}".format( + "InitGui failed to register routing connection panel command:\n{0}".format( traceback_module.format_exc() ) ) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 010be69..43862d7 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2,9 +2,10 @@ # # 这个模块只管理 FreeCAD 文档里的走线网络,不写数据库。 # 第一版的思路是:用户或模板把线槽/导轨中心线标成 carrier, -# 自动布线算法再沿这些 carrier 做最短路搜索。 +# 布线连接算法再沿这些 carrier 做最短路搜索。 import heapq +import json import math import FreeCAD as App @@ -16,6 +17,8 @@ import WiringObjects ROUTING_ROLE = "RoutingCarrier" ROUTE_CARRIER_KIND = "RoutingPath" ROUTE_CARRIER_KIND_WIRE_DUCT = "WireDuct" +ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END = "WireDuctOpenEnd" +ROUTE_CARRIER_KIND_WIRING_CUT_OUT = "WiringCutOut" ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath" ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange" ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess" @@ -25,9 +28,11 @@ DEFAULT_SURFACE_LANE_SPACING = 100.0 DEFAULT_SURFACE_OFFSET = 5.0 DEFAULT_SURFACE_MARGIN = 20.0 DEFAULT_WIRE_DUCT_MARGIN = 20.0 +DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH = 20.0 DEFAULT_ROUTE_PATH_FACE_OFFSET = 2.0 DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT = 2.5 DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE = 1000.0 +DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0 WIRE_DUCT_OBSTACLE_MODE = "PassThrough" SUPPORT_SURFACE_OBSTACLE_MODE = "SupportSurface" WIRE_DUCT_NAME_KEYWORDS = ( @@ -57,6 +62,21 @@ WIRE_DUCT_EXCLUDE_KEYWORDS = ( "背板", "底板", ) +WIRING_CUT_OUT_NAME_KEYWORDS = ( + "wiring cut-out", + "wiring cutout", + "wire cut-out", + "wire cutout", + "cable cut-out", + "cable cutout", + "through hole", + "pass-through", + "passthrough", + "穿线孔", + "过线孔", + "开孔", + "过线", +) SUPPORT_SURFACE_NAME_KEYWORDS = ( "mounting plate", "base plate", @@ -83,12 +103,12 @@ SUPPORT_SURFACE_CARRIER_KINDS = { } DEFAULT_KIND_COST_FACTORS = { ROUTE_CARRIER_KIND_WIRE_DUCT: 1.0, + ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END: 1.0, + ROUTE_CARRIER_KIND_WIRING_CUT_OUT: 1.0, ROUTE_CARRIER_KIND: 1.0, ROUTE_CARRIER_KIND_AUXILIARY_PATH: 2.0, ROUTE_CARRIER_KIND_TERMINAL_ACCESS: 2.0, ROUTE_CARRIER_KIND_ROUTING_RANGE: 8.0, - # 旧文档兼容:之前的贴面网格使用 SurfaceGrid。 - "SurfaceGrid": 8.0, "UserPath": 1.0, } ROUTE_CARRIER_VIEW_STYLES = { @@ -96,6 +116,14 @@ ROUTE_CARRIER_VIEW_STYLES = { "color": (1.0, 0.55, 0.0), "width": 4.0, }, + ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END: { + "color": (1.0, 0.72, 0.2), + "width": 3.0, + }, + ROUTE_CARRIER_KIND_WIRING_CUT_OUT: { + "color": (0.0, 0.72, 0.85), + "width": 3.0, + }, ROUTE_CARRIER_KIND_ROUTING_RANGE: { "color": (0.0, 0.65, 0.35), "width": 1.0, @@ -198,6 +226,19 @@ def _scale(vector, factor): ) +def _closest_point_on_segment(point, start, end): + target = _vector(point) + start = _vector(start) + end = _vector(end) + segment = _subtract(end, start) + length_squared = _dot(segment, segment) + if length_squared <= DEFAULT_NODE_TOLERANCE * DEFAULT_NODE_TOLERANCE: + return start + parameter = _dot(_subtract(target, start), segment) / length_squared + parameter = max(0.0, min(1.0, parameter)) + return _add(start, _scale(segment, parameter)) + + def _dot(left, right): return ( float(left.x) * float(right.x) @@ -394,12 +435,69 @@ def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND): obj, "CanRouteWire", PROPERTY_GROUP, - "Whether auto-routing can use this path", + "Whether routing connections can use this path", True, ) return obj +def _set_wire_duct_source_semantics(source): + if source is None: + return + TerminalObjects.ensure_string_property( + source, + "QetRoutingSourceKind", + PROPERTY_GROUP, + "Routing source kind", + ROUTE_CARRIER_KIND_WIRE_DUCT, + ) + TerminalObjects.ensure_string_property( + source, + "QetRoutingObstacleMode", + PROPERTY_GROUP, + "How routing connection collision checks should treat this object", + WIRE_DUCT_OBSTACLE_MODE, + ) + + +def _set_support_surface_source_semantics(source): + if source is None: + return + TerminalObjects.ensure_string_property( + source, + "QetRoutingSourceKind", + PROPERTY_GROUP, + "Routing source kind", + ROUTE_CARRIER_KIND_ROUTING_RANGE, + ) + TerminalObjects.ensure_string_property( + source, + "QetRoutingObstacleMode", + PROPERTY_GROUP, + "How routing connection collision checks should treat this object", + SUPPORT_SURFACE_OBSTACLE_MODE, + ) + + +def _set_wiring_cut_out_source_semantics(source): + if source is None: + return + TerminalObjects.ensure_string_property( + source, + "QetRoutingSourceKind", + PROPERTY_GROUP, + "Routing source kind", + ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ) + TerminalObjects.ensure_string_property( + source, + "QetRoutingObstacleMode", + PROPERTY_GROUP, + "How routing connection collision checks should treat this object", + WIRE_DUCT_OBSTACLE_MODE, + ) + + def _style_route_carrier(carrier, kind): style = ROUTE_CARRIER_VIEW_STYLES.get(kind) or ROUTE_CARRIER_VIEW_STYLES[ROUTE_CARRIER_KIND] try: @@ -878,6 +976,27 @@ def _is_support_surface_candidate(obj): return _is_thin_surface_bbox(bbox) +def _is_wiring_cut_out_candidate(obj): + if obj is None: + return False + if is_route_carrier(obj) or TerminalObjects.is_terminal_object(obj): + return False + if (getattr(obj, "RouteType", "") or "").strip(): + return False + + source_kind = (getattr(obj, "QetRoutingSourceKind", "") or "").strip() + carrier_kind = (getattr(obj, "QetCarrierKind", "") or "").strip().lower() + has_semantic_hint = ( + source_kind == ROUTE_CARRIER_KIND_WIRING_CUT_OUT + or carrier_kind in {"wiring_cut_out", "wiring_cutout", "wire_cutout"} + ) + text = _routing_source_text(obj) + has_name_hint = any(keyword in text for keyword in WIRING_CUT_OUT_NAME_KEYWORDS) + if not has_semantic_hint and not has_name_hint: + return False + return _bound_box_from_object(obj) is not None + + def _support_face_from_bbox(bbox): extents = _bbox_extents(bbox) normal_axis = min(extents, key=extents.get) @@ -1179,7 +1298,7 @@ def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUT return created -def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): +def _wire_duct_centerline_spec_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): extents = { axis: _bbox_extent(bbox, axis) for axis in ("x", "y", "z") @@ -1187,10 +1306,10 @@ def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_a main_axis = max(extents, key=extents.get) sorted_extents = sorted(extents.values(), reverse=True) if sorted_extents[0] <= DEFAULT_NODE_TOLERANCE: - return [] + return {"centerline": [], "open_ends": []} if len(sorted_extents) > 1 and sorted_extents[1] > DEFAULT_NODE_TOLERANCE: if sorted_extents[0] / sorted_extents[1] < float(min_aspect or 1.0): - return [] + return {"centerline": [], "open_ends": []} low, high = _bbox_axis_range(bbox, main_axis) center = _bbox_center(bbox) @@ -1200,6 +1319,64 @@ def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_a start = _set_axis(center, main_axis, low + usable_margin) end = _set_axis(center, main_axis, high - usable_margin) + if _distance(start, end) <= DEFAULT_NODE_TOLERANCE: + return {"centerline": [], "open_ends": []} + + cross_axes = sorted( + [axis for axis in ("x", "y", "z") if axis != main_axis], + key=lambda axis: _bbox_extent(bbox, axis), + reverse=True, + ) + open_ends = [] + if cross_axes: + cross_axis = cross_axes[0] + cross_extent = _bbox_extent(bbox, cross_axis) + half_length = max( + min(cross_extent * 0.5, float(margin or DEFAULT_WIRE_DUCT_MARGIN)), + min(cross_extent * 0.5, DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH * 0.5), + ) + if half_length > DEFAULT_NODE_TOLERANCE: + for endpoint in (start, end): + open_ends.append( + [ + _set_axis(endpoint, cross_axis, _axis_value(center, cross_axis) - half_length), + _set_axis(endpoint, cross_axis, _axis_value(center, cross_axis) + half_length), + ] + ) + + return { + "centerline": [start, end], + "open_ends": open_ends, + "main_axis": main_axis, + } + + +def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): + return _wire_duct_centerline_spec_from_bbox( + bbox, + margin=margin, + min_aspect=min_aspect, + ).get("centerline", []) + + +def _wiring_cut_out_points_from_bbox(bbox): + extents = _bbox_extents(bbox) + if not extents: + return [] + through_axis = min(extents, key=extents.get) + low, high = _bbox_axis_range(bbox, through_axis) + center = _bbox_center(bbox) + if abs(high - low) <= DEFAULT_NODE_TOLERANCE: + other_extents = [ + _bbox_extent(bbox, axis) + for axis in ("x", "y", "z") + if axis != through_axis + ] + fallback = max(other_extents or [DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH]) + low = _axis_value(center, through_axis) - fallback * 0.5 + high = _axis_value(center, through_axis) + fallback * 0.5 + start = _set_axis(center, through_axis, low) + end = _set_axis(center, through_axis, high) if _distance(start, end) <= DEFAULT_NODE_TOLERANCE: return [] return [start, end] @@ -1226,27 +1403,15 @@ def _mark_wire_duct_source(source, carrier): if source is None: return try: - TerminalObjects.ensure_string_property( - source, - "QetRoutingSourceKind", - PROPERTY_GROUP, - "Routing source kind", - ROUTE_CARRIER_KIND_WIRE_DUCT, - ) - TerminalObjects.ensure_string_property( - source, - "QetRoutingObstacleMode", - PROPERTY_GROUP, - "How auto-routing collision checks should treat this object", - WIRE_DUCT_OBSTACLE_MODE, - ) - TerminalObjects.ensure_string_property( - source, - "QetRouteCarrierName", - PROPERTY_GROUP, - "Generated route carrier for this source", - getattr(carrier, "Name", ""), - ) + _set_wire_duct_source_semantics(source) + if carrier is not None: + TerminalObjects.ensure_string_property( + source, + "QetRouteCarrierName", + PROPERTY_GROUP, + "Generated route carrier for this source", + getattr(carrier, "Name", ""), + ) except Exception: pass @@ -1255,26 +1420,29 @@ def _mark_support_surface_source(source, carriers): if source is None or not carriers: return try: + _set_support_surface_source_semantics(source) TerminalObjects.ensure_string_property( source, - "QetRoutingSourceKind", - PROPERTY_GROUP, - "Routing source kind", - ROUTE_CARRIER_KIND_ROUTING_RANGE, - ) - TerminalObjects.ensure_string_property( - source, - "QetRoutingObstacleMode", + "QetRouteCarrierName", PROPERTY_GROUP, - "How auto-routing collision checks should treat this object", - SUPPORT_SURFACE_OBSTACLE_MODE, + "Generated route carrier for this source", + getattr(carriers[0], "Name", ""), ) + except Exception: + pass + + +def _mark_wiring_cut_out_source(source, carrier): + if source is None or carrier is None: + return + try: + _set_wiring_cut_out_source_semantics(source) TerminalObjects.ensure_string_property( source, "QetRouteCarrierName", PROPERTY_GROUP, "Generated route carrier for this source", - getattr(carriers[0], "Name", ""), + getattr(carrier, "Name", ""), ) except Exception: pass @@ -1338,6 +1506,64 @@ def detect_support_surface_sources(doc): return sources +def detect_wiring_cut_out_sources(doc): + """Return pass-through cut-out objects that can bridge routing carriers.""" + sources = [] + seen = set() + for obj in list(getattr(doc, "Objects", []) or []): + if id(obj) in seen: + continue + seen.add(id(obj)) + if _is_wiring_cut_out_candidate(obj): + sources.append(obj) + return sources + + +def prepare_layout_space_sources_from_document(doc, project_uuid=""): + """Normalize the current FreeCAD document as an EPLAN-style layout space. + + This does not generate the routing path network. It marks source objects so + wire ducts are pass-through objects, support panels can become routing ranges, + and the wiring buckets exist before network generation or routing. + """ + if doc is None: + raise RoutingNetworkError("No FreeCAD document is available.") + + WiringObjects.ensure_wiring_root_group(doc, project_uuid) + + wire_duct_sources = detect_wire_duct_sources(doc) + support_surface_sources = detect_support_surface_sources(doc) + wiring_cut_out_sources = detect_wiring_cut_out_sources(doc) + for source in wire_duct_sources: + try: + _set_wire_duct_source_semantics(source) + except Exception: + pass + for source in support_surface_sources: + try: + _set_support_surface_source_semantics(source) + except Exception: + pass + for source in wiring_cut_out_sources: + try: + _set_wiring_cut_out_source_semantics(source) + except Exception: + pass + + try: + doc.recompute() + except Exception: + pass + + return { + "wire_duct_sources": len(wire_duct_sources), + "support_surface_sources": len(support_surface_sources), + "wiring_cut_out_sources": len(wiring_cut_out_sources), + "routable_terminals": len(_collect_routable_terminals(doc)), + "existing_network": network_summary(doc), + } + + def create_wire_duct_carriers_from_document( doc, project_uuid="", @@ -1352,11 +1578,12 @@ def create_wire_duct_carriers_from_document( bbox = _bound_box_from_object(source) if bbox is None: continue - points = _wire_duct_centerline_from_bbox( + spec = _wire_duct_centerline_spec_from_bbox( bbox, margin=margin, min_aspect=min_aspect, ) + points = spec.get("centerline", []) if len(points) < 2: continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" @@ -1369,6 +1596,43 @@ def create_wire_duct_carriers_from_document( ) _mark_wire_duct_source(source, carrier) created.append(carrier) + for end_index, open_end_points in enumerate(spec.get("open_ends", []) or [], start=1): + if len(open_end_points) < 2: + continue + created.append( + create_route_carrier( + doc, + open_end_points, + label="QET Auto Wire Duct Open End {0} {1}".format(label, end_index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + ) + ) + return created + + +def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""): + """Create pass-through route carriers for wiring cut-out objects.""" + created = [] + for source in detect_wiring_cut_out_sources(doc): + if _live_source_carrier(doc, source) is not None: + continue + bbox = _bound_box_from_object(source) + if bbox is None: + continue + points = _wiring_cut_out_points_from_bbox(bbox) + if len(points) < 2: + continue + label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wiring Cut-Out" + carrier = create_route_carrier( + doc, + points, + label="QET Auto Wiring Cut-Out {0}".format(label), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ) + _mark_wiring_cut_out_source(source, carrier) + created.append(carrier) return created @@ -1502,21 +1766,20 @@ def create_terminal_access_carriers_from_document( if network.get("segment_count", 0) <= 0: return [] - nodes = network.get("nodes", {}) or {} created = [] for terminal in _collect_routable_terminals(doc): if _live_source_carrier(doc, terminal) is not None: continue exit_point = _terminal_exit_point(terminal, terminal_exit_length) - nearest_key, distance = nearest_node(network, exit_point) - if nearest_key is None: + nearest_point, distance = nearest_point_on_network(network, exit_point) + if nearest_point is None: continue if max_distance and float(distance or 0.0) > float(max_distance): continue if float(distance or 0.0) <= DEFAULT_NODE_TOLERANCE: continue - points = _orthogonal_access_points(exit_point, nodes[nearest_key]) + points = _orthogonal_access_points(exit_point, nearest_point) if len(points) < 2: continue label = getattr(terminal, "Label", "") or getattr(terminal, "Name", "") or "Terminal" @@ -1532,21 +1795,38 @@ def create_terminal_access_carriers_from_document( return created -def create_layout_space_from_document( +def create_routing_path_network_from_document( doc, project_uuid="", + selection_ex=None, terminal_exit_length=20.0, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, ): - """Prepare routing layout space from the full 3D assembly. + """Generate the EPLAN-style routing path network for the layout space. - This is the production/EPLAN-style path: use the FreeCAD document as the - layout-space source, not selected faces or Draft sketches. + Selection is treated as a hint for wire ducts that cannot be detected from + names or semantics. The full document is still scanned afterwards, matching + EPLAN's "generate routing path network for the layout space" behavior. """ + layout_space = prepare_layout_space_sources_from_document( + doc, + project_uuid=project_uuid, + ) + selected_wire_ducts = [] + if selection_ex: + selected_wire_ducts = create_wire_duct_carriers_from_selection( + doc, + selection_ex, + project_uuid=project_uuid, + ) wire_ducts = create_wire_duct_carriers_from_document( doc, project_uuid=project_uuid, ) + cut_outs = create_wiring_cut_out_carriers_from_document( + doc, + project_uuid=project_uuid, + ) surfaces = create_surface_carriers_from_document( doc, project_uuid=project_uuid, @@ -1557,10 +1837,31 @@ def create_layout_space_from_document( terminal_exit_length=terminal_exit_length, max_distance=terminal_access_max_distance, ) + all_wire_duct_created = list(selected_wire_ducts) + list(wire_ducts) + wire_duct_main_count = sum( + 1 + for carrier in all_wire_duct_created + if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT + ) + selected_wire_duct_main_count = sum( + 1 + for carrier in selected_wire_ducts + if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT + ) + open_end_count = sum( + 1 + for carrier in all_wire_duct_created + if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() + == ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END + ) return { - "wire_duct_carriers": len(wire_ducts), + "wire_duct_carriers": wire_duct_main_count, + "selected_wire_duct_carriers": selected_wire_duct_main_count, + "wire_duct_open_end_carriers": open_end_count, + "wiring_cut_out_carriers": len(cut_outs), "surface_carriers": len(surfaces), "terminal_access_carriers": len(terminal_access), + "layout_space": layout_space, "network": network_summary(doc), } @@ -1578,11 +1879,12 @@ def create_wire_duct_carriers_from_selection( bbox = _bound_box_from_object(source) if bbox is None: continue - points = _wire_duct_centerline_from_bbox( + spec = _wire_duct_centerline_spec_from_bbox( bbox, margin=margin, min_aspect=min_aspect, ) + points = spec.get("centerline", []) if len(points) < 2: continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" @@ -1595,6 +1897,18 @@ def create_wire_duct_carriers_from_selection( ) _mark_wire_duct_source(source, carrier) created.append(carrier) + for end_index, open_end_points in enumerate(spec.get("open_ends", []) or [], start=1): + if len(open_end_points) < 2: + continue + created.append( + create_route_carrier( + doc, + open_end_points, + label="QET Wire Duct Open End {0} {1}".format(label, end_index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + ) + ) return created @@ -1679,15 +1993,22 @@ def _carrier_cost_factor(carrier, kind_cost_factors=None): return 1.0 -def build_route_graph(doc, tolerance=DEFAULT_NODE_TOLERANCE, blocked_bboxes=None): +def build_route_graph( + doc, + tolerance=DEFAULT_NODE_TOLERANCE, + blocked_bboxes=None, + adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, +): """Build an undirected graph from every enabled route carrier.""" nodes = {} edges = {} carriers = collect_route_carriers(doc) segment_count = 0 blocked_segment_count = 0 + bridged_segment_count = 0 blocked_bboxes = list(blocked_bboxes or []) segments = [] + wire_duct_endpoint_nodes = [] def ensure_node(point): key = _point_key(point, tolerance=tolerance) @@ -1741,6 +2062,11 @@ def build_route_graph(doc, tolerance=DEFAULT_NODE_TOLERANCE, blocked_bboxes=None ) if len(ordered) < 2: continue + carrier = segment["carrier"] + if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT: + for endpoint in (ordered[0], ordered[-1]): + endpoint_key = ensure_node(endpoint) + wire_duct_endpoint_nodes.append((endpoint_key, nodes[endpoint_key], carrier)) previous_key = ensure_node(ordered[0]) previous_point = nodes[previous_key] for point in ordered[1:]: @@ -1753,19 +2079,44 @@ def build_route_graph(doc, tolerance=DEFAULT_NODE_TOLERANCE, blocked_bboxes=None previous_key = current_key previous_point = current_point continue - carrier = segment["carrier"] edges[previous_key].append((current_key, weight, carrier)) edges[current_key].append((previous_key, weight, carrier)) segment_count += 1 previous_key = current_key previous_point = current_point + adjoining_limit = max(float(adjoining_duct_tolerance or 0.0), 0.0) + bridged_pairs = set() + if adjoining_limit > tolerance: + for left_index, left in enumerate(wire_duct_endpoint_nodes): + left_key, left_point, left_carrier = left + for right_key, right_point, right_carrier in wire_duct_endpoint_nodes[left_index + 1:]: + if left_key == right_key or left_carrier is right_carrier: + continue + pair = tuple(sorted((left_key, right_key))) + if pair in bridged_pairs: + continue + distance = _distance(left_point, right_point) + if distance <= tolerance or distance > adjoining_limit: + continue + if any(next_key == right_key for next_key, _weight, _carrier in edges.get(left_key, [])): + continue + if _segment_hits_blocked_bbox(left_point, right_point, blocked_bboxes): + blocked_segment_count += 1 + continue + edges[left_key].append((right_key, distance, left_carrier)) + edges[right_key].append((left_key, distance, right_carrier)) + segment_count += 1 + bridged_segment_count += 1 + bridged_pairs.add(pair) + return { "nodes": nodes, "edges": edges, "carriers": carriers, "carrier_count": len(carriers), "segment_count": segment_count, + "bridged_segment_count": bridged_segment_count, "blocked_segment_count": blocked_segment_count, "tolerance": tolerance, } @@ -1786,12 +2137,64 @@ def nearest_node(network, point): return best_key, best_distance -def shortest_path(network, start_key, end_key, bend_penalty=0.0, kind_cost_factors=None): +def nearest_point_on_network(network, point): + """Return the closest point on any route-network edge. + + The point may lie in the middle of a carrier segment. If a TerminalAccess + carrier ends there, the next graph build will split the crossed segment at + that point and create an EPLAN-like jump-in routing point. + """ + if not isinstance(network, dict): + return None, None + nodes = network.get("nodes", {}) or {} + edges = network.get("edges", {}) or {} + if not nodes or not edges: + return None, None + + target = _vector(point) + best_point = None + best_distance = None + seen = set() + for key, neighbors in edges.items(): + start = nodes.get(key) + if start is None: + continue + for next_key, _weight, _carrier in neighbors: + pair = tuple(sorted((key, next_key))) + if pair in seen: + continue + seen.add(pair) + end = nodes.get(next_key) + if end is None: + continue + candidate = _closest_point_on_segment(target, start, end) + distance = _distance(target, candidate) + if best_distance is None or distance < best_distance: + best_point = candidate + best_distance = distance + if best_point is not None: + return best_point, best_distance + return nearest_node(network, target) + + +def _carrier_track_payload(carrier): + return { + "name": getattr(carrier, "Name", ""), + "label": getattr(carrier, "Label", ""), + "kind": (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND, + } + + +def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, kind_cost_factors=None): """Dijkstra search with a small extra cost when route direction changes.""" if start_key is None or end_key is None: return None if start_key == end_key: - return [start_key] + return { + "path": [start_key], + "segments": [], + "cost": 0.0, + } nodes = network.get("nodes", {}) edges = network.get("edges", {}) @@ -1809,12 +2212,46 @@ def shortest_path(network, start_key, end_key, bend_penalty=0.0, kind_cost_facto continue if key == end_key: path = [key] + segments = [] current_state = state while current_state in previous: - current_state = previous[current_state] + previous_entry = previous[current_state] + previous_state = previous_entry["state"] + previous_key = previous_state[0] + current_key = current_state[0] + carrier = previous_entry.get("carrier") + segments.append( + { + "from_key": list(previous_key), + "to_key": list(current_key), + "from": _point_payload(nodes[previous_key]), + "to": _point_payload(nodes[current_key]), + "length_mm": float(previous_entry.get("weight", 0.0) or 0.0), + "carrier": _carrier_track_payload(carrier), + } + ) + current_state = previous_state path.append(current_state[0]) path.reverse() - return path + segments.reverse() + + carrier_names = [] + carrier_kinds = {} + for segment in segments: + carrier = segment.get("carrier", {}) + name = carrier.get("name", "") + if name and name not in carrier_names: + carrier_names.append(name) + kind = carrier.get("kind", "") or ROUTE_CARRIER_KIND + carrier_kinds[kind] = carrier_kinds.get(kind, 0) + 1 + + return { + "path": path, + "segments": segments, + "carrier_names": carrier_names, + "carrier_kinds": carrier_kinds, + "cost": float(cost), + } for next_key, weight, carrier in edges.get(key, []): direction = _direction_key(nodes[key], nodes[next_key]) @@ -1825,7 +2262,11 @@ def shortest_path(network, start_key, end_key, bend_penalty=0.0, kind_cost_facto next_cost = cost + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) + bend_cost if next_cost < distances.get(next_state, float("inf")): distances[next_state] = next_cost - previous[next_state] = state + previous[next_state] = { + "state": state, + "carrier": carrier, + "weight": weight, + } counter += 1 heapq.heappush(queue, (next_cost, counter, next_key, direction)) @@ -1839,6 +2280,10 @@ def path_points(network, path_keys): def network_summary(doc): network = build_route_graph(doc) + return _network_summary_from_graph(network) + + +def _network_summary_from_graph(network): kinds = {} for carrier in network.get("carriers", []) or []: kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND @@ -1846,12 +2291,263 @@ def network_summary(doc): return { "carriers": int(network.get("carrier_count", 0)), "segments": int(network.get("segment_count", 0)), + "bridged_segments": int(network.get("bridged_segment_count", 0)), "blocked_segments": int(network.get("blocked_segment_count", 0)), "nodes": len(network.get("nodes", {})), "kinds": kinds, } +def _route_graph_components(network): + nodes = network.get("nodes", {}) or {} + edges = network.get("edges", {}) or {} + seen = set() + components = [] + + for start_key in nodes.keys(): + if start_key in seen: + continue + stack = [start_key] + seen.add(start_key) + node_keys = [] + edge_pairs = set() + carriers = {} + kinds = {} + + while stack: + key = stack.pop() + node_keys.append(key) + for next_key, _weight, carrier in edges.get(key, []) or []: + pair = tuple(sorted((key, next_key))) + edge_pairs.add(pair) + if carrier is not None: + carrier_name = getattr(carrier, "Name", "") + if carrier_name: + carriers[carrier_name] = _carrier_track_payload(carrier) + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + kinds[kind] = kinds.get(kind, 0) + 1 + if next_key not in seen: + seen.add(next_key) + stack.append(next_key) + + components.append( + { + "index": len(components), + "nodes": len(node_keys), + "segments": len(edge_pairs), + "carrier_names": sorted(carriers.keys()), + "carrier_kinds": kinds, + "has_terminal_access": any( + carrier.get("kind") == ROUTE_CARRIER_KIND_TERMINAL_ACCESS + for carrier in carriers.values() + ), + } + ) + + return components + + +def _wire_duct_endpoint_breaks(network): + nodes = network.get("nodes", {}) or {} + edges = network.get("edges", {}) or {} + breaks = [] + for carrier in network.get("carriers", []) or []: + if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() != ROUTE_CARRIER_KIND_WIRE_DUCT: + continue + points = _carrier_points(carrier) + if len(points) < 2: + continue + for endpoint in (points[0], points[-1]): + key = _point_key(endpoint, tolerance=network.get("tolerance", DEFAULT_NODE_TOLERANCE)) + degree = len(edges.get(key, []) or []) + if degree > 1: + continue + breaks.append( + { + "carrier": _carrier_track_payload(carrier), + "point": _point_payload(nodes.get(key, endpoint)), + "degree": degree, + } + ) + return breaks + + +def _terminal_diagnostic_payload(terminal): + return { + "name": getattr(terminal, "Name", ""), + "label": getattr(terminal, "Label", ""), + "terminal_uuid": (getattr(terminal, "QetTerminalUuid", "") or "").strip(), + "instance_id": (getattr(terminal, "QetInstanceId", "") or "").strip(), + } + + +def diagnose_routing_path_network( + doc, + terminal_exit_length=20.0, + terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, +): + """Inspect the generated routing path network without routing wires.""" + if doc is None: + raise RoutingNetworkError("No FreeCAD document is available.") + + network = build_route_graph(doc) + components = _route_graph_components(network) + summary = _network_summary_from_graph(network) + isolated_components = components if len(components) > 1 else [] + unconnected_terminals = [] + + max_distance = max(float(terminal_access_max_distance or 0.0), 0.0) + for terminal in _collect_routable_terminals(doc): + exit_point = _terminal_exit_point(terminal, terminal_exit_length) + nearest_point, distance = nearest_point_on_network(network, exit_point) + access_carrier = _live_source_carrier(doc, terminal) + access_live = access_carrier is not None and is_route_carrier(access_carrier) + too_far = nearest_point is None or (max_distance > 0.0 and float(distance or 0.0) > max_distance) + connected_directly = nearest_point is not None and float(distance or 0.0) <= DEFAULT_NODE_TOLERANCE + if (access_live or connected_directly) and not too_far: + continue + payload = _terminal_diagnostic_payload(terminal) + payload.update( + { + "access_carrier": getattr(access_carrier, "Name", "") if access_carrier is not None else "", + "nearest_network_distance_mm": None if distance is None else float(distance), + "nearest_network_point": None if nearest_point is None else _point_payload(nearest_point), + "code": "terminal_access_missing" if not access_live else "terminal_access_too_far", + } + ) + unconnected_terminals.append(payload) + + possible_breaks = _wire_duct_endpoint_breaks(network) + issues = [] + if isolated_components: + issues.append( + { + "severity": "warning", + "code": "isolated_network_components", + "message": "Routing path network contains isolated components.", + "count": len(isolated_components), + } + ) + if unconnected_terminals: + issues.append( + { + "severity": "error", + "code": "unconnected_terminals", + "message": "Some terminals are not connected to the routing path network.", + "count": len(unconnected_terminals), + } + ) + if possible_breaks: + issues.append( + { + "severity": "warning", + "code": "wire_duct_endpoint_breaks", + "message": "Some wire duct endpoints have no adjacent network connection.", + "count": len(possible_breaks), + } + ) + + return { + "summary": summary, + "component_count": len(components), + "components": components, + "isolated_components": isolated_components, + "unconnected_terminals": unconnected_terminals, + "possible_breaks": possible_breaks, + "issues": issues, + "ok": not issues, + } + + +def _highlight_routing_network_diagnostics(doc, diagnostic): + isolated_carriers = set() + for component in diagnostic.get("isolated_components", []) or []: + isolated_carriers.update(component.get("carrier_names", []) or []) + + unconnected_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("unconnected_terminals", []) or [] + if item.get("name", "") + ) + break_carriers = set( + item.get("carrier", {}).get("name", "") + for item in diagnostic.get("possible_breaks", []) or [] + if item.get("carrier", {}).get("name", "") + ) + + for obj in list(getattr(doc, "Objects", []) or []): + name = getattr(obj, "Name", "") + try: + if name in unconnected_terminal_names: + obj.ViewObject.LineColor = (1.0, 0.0, 0.0) + obj.ViewObject.LineWidth = 4.0 + elif name in break_carriers: + obj.ViewObject.LineColor = (1.0, 0.0, 0.0) + obj.ViewObject.LineWidth = 4.0 + elif name in isolated_carriers: + obj.ViewObject.LineColor = (1.0, 0.35, 0.0) + obj.ViewObject.LineWidth = 3.0 + except Exception: + pass + + +def _clear_routing_path_network_diagnostics(doc, group): + removed = 0 + for obj in list(getattr(group, "Group", []) or []): + if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "RoutingPathNetwork": + continue + _detach_from_groups(doc, obj) + try: + if doc.getObject(getattr(obj, "Name", "")) is not None: + doc.removeObject(obj.Name) + removed += 1 + except Exception: + pass + return removed + + +def write_routing_path_network_diagnostic( + doc, + project_uuid="", + terminal_exit_length=20.0, + terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, +): + diagnostic = diagnose_routing_path_network( + doc, + terminal_exit_length=terminal_exit_length, + terminal_access_max_distance=terminal_access_max_distance, + ) + group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) + _clear_routing_path_network_diagnostics(doc, group) + + obj = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETRoutingPathNetworkDiagnostic")) + obj.Label = "QET Routing Path Network Diagnostic" + TerminalObjects.ensure_string_property( + obj, + "QetDiagnosticKind", + PROPERTY_GROUP, + "QET diagnostic kind", + "RoutingPathNetwork", + ) + TerminalObjects.ensure_string_property( + obj, + "QetDiagnosticJson", + PROPERTY_GROUP, + "QET routing path network diagnostic payload", + json.dumps(diagnostic, ensure_ascii=False), + ) + group.addObject(obj) + _highlight_routing_network_diagnostics(doc, diagnostic) + try: + doc.recompute() + except Exception: + pass + return { + "diagnostic": diagnostic, + "diagnostic_object": obj, + } + + def carrier_payload(carrier): return { "name": getattr(carrier, "Name", ""), diff --git a/src/Mod/FreeCADExchange/WiringObjects.py b/src/Mod/FreeCADExchange/WiringObjects.py index 17cb256..719e83f 100644 --- a/src/Mod/FreeCADExchange/WiringObjects.py +++ b/src/Mod/FreeCADExchange/WiringObjects.py @@ -304,6 +304,16 @@ def _json_array_property(obj, prop_name): return [] +def _json_property(obj, prop_name, fallback=None): + text = getattr(obj, prop_name, "") + if not text: + return fallback + try: + return json.loads(text) + except Exception: + return fallback + + def wire_shape_points(wire_obj): if wire_obj is None: return [] @@ -362,6 +372,7 @@ def wire_payload_from_object(wire_obj): "points": [], "manual_waypoints": [], "route_nodes": [], + "route_track": {}, "terminal_exit_length": float(getattr(wire_obj, "QetTerminalExitLength", 0.0) or 0.0), } points = [_point_from_vector(point) for point in wire_shape_points(wire_obj)] @@ -382,6 +393,7 @@ def wire_payload_from_object(wire_obj): "points": points, "manual_waypoints": _json_array_property(wire_obj, "QetManualWaypointsJson"), "route_nodes": _json_array_property(wire_obj, "QetRouteNodesJson"), + "route_track": _json_property(wire_obj, "QetRouteTrackJson", {}), "terminal_exit_length": float(getattr(wire_obj, "QetTerminalExitLength", 0.0) or 0.0), } @@ -393,7 +405,7 @@ def is_routed_wire_object(obj): return ( "QetStartTerminalUuid" in properties and "QetEndTerminalUuid" in properties - and (getattr(obj, "RouteType", "") or "").strip() in {"Manual", "GuidedManual", "AutoSuggested"} + and (getattr(obj, "RouteType", "") or "").strip() in {"Manual", "GuidedManual", "RoutedConnection"} ) diff --git a/tests/manual/freecad_auto_routing_smoke.py b/tests/manual/freecad_auto_routing_smoke.py index 039feef..0b14aed 100644 --- a/tests/manual/freecad_auto_routing_smoke.py +++ b/tests/manual/freecad_auto_routing_smoke.py @@ -8,8 +8,8 @@ import FreeCAD as App REPO_ROOT = r"D:\LightWork3D" MODULE_DIR = os.path.join(REPO_ROOT, "src", "Mod", "FreeCADExchange") OUT_DIR = os.path.join(REPO_ROOT, "tests", "out") -OUT_FCSTD = os.path.join(OUT_DIR, "auto_routing_smoke.FCStd") -OUT_JSON = os.path.join(OUT_DIR, "auto_routing_smoke_result.json") +OUT_FCSTD = os.path.join(OUT_DIR, "routing_connection_smoke.FCStd") +OUT_JSON = os.path.join(OUT_DIR, "routing_connection_smoke_result.json") if MODULE_DIR not in sys.path: sys.path.insert(0, MODULE_DIR) @@ -48,7 +48,7 @@ def _point_payload(point): def main(): os.makedirs(OUT_DIR, exist_ok=True) - doc = App.newDocument("AutoRoutingSmoke") + doc = App.newDocument("RoutingConnectionSmoke") App.setActiveDocument(doc.Name) TerminalObjects.ensure_root_group(doc, "project-smoke") WiringObjects.initialize_wiring_scene(doc, "project-smoke") @@ -76,7 +76,7 @@ def main(): obstacle.Placement = App.Placement(App.Vector(60, -20, -10), App.Rotation()) doc.recompute() - result = AutoRouting.route_between_terminals(doc, start, end) + result = AutoRouting.route_eplan_connection_between_terminals(doc, start, end) payload = { "algorithm": result["algorithm"], "route_status": result["route_status"], @@ -92,8 +92,8 @@ def main(): routed_group = reopened.getObject("QETWiring_04_Routed") reopened_wires = list(getattr(routed_group, "Group", []) or []) if routed_group else [] payload["reopened_routed_wire_count"] = len(reopened_wires) - payload["reopened_has_auto_route"] = any( - (getattr(wire, "RouteType", "") or "").strip() == "AutoSuggested" + payload["reopened_has_routed_connection"] = any( + (getattr(wire, "RouteType", "") or "").strip() == "RoutedConnection" for wire in reopened_wires ) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 37b6d37..f57050c 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -210,7 +210,7 @@ def _terminal(doc, terminal_objects, name, terminal_uuid, point): class AutoRoutingTest(unittest.TestCase): - def test_auto_route_selected_terminals_requires_supported_route_by_default(self): + def test_eplan_connection_route_selected_terminals_requires_supported_route_by_default(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -220,33 +220,9 @@ class AutoRoutingTest(unittest.TestCase): end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 20, 0)) with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_between_terminals(doc, start, end) + auto_routing.route_eplan_connection_between_terminals(doc, start, end) - def test_auto_route_can_still_use_explicit_floating_fallback_for_debug(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, 20, 0)) - - result = auto_routing.route_between_terminals( - doc, - start, - end, - options={"allow_floating_fallback": True}, - ) - - wire = result["wire"] - self.assertEqual("orthogonal-v1", result["algorithm"]) - self.assertEqual("AutoSuggested", wire.RouteType) - self.assertEqual("Auto", wire.RouteMode) - self.assertEqual("Routed", wire.RouteStatus) - self.assertGreaterEqual(len(wire.Points), 4) - self.assertIn(wire, doc.getObject("QETWiring_04_Routed").Group) - - def test_auto_route_prefers_user_route_carrier_network(self): + def test_eplan_connection_route_prefers_user_route_carrier_network(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -265,13 +241,13 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", ) - result = auto_routing.route_between_terminals(doc, start, end) + 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.assertTrue(any(point.y == 30.0 for point in result["points"])) - def test_auto_route_stores_length_and_wire_style_diagnostics(self): + def test_eplan_connection_route_stores_length_and_wire_style_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -286,7 +262,7 @@ class AutoRoutingTest(unittest.TestCase): kind="WireDuct", ) - result = auto_routing.route_between_terminals( + result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, @@ -295,14 +271,17 @@ class AutoRoutingTest(unittest.TestCase): options={"wire_style_id": "42"}, ) wire = result["wire"] - payload = json.loads(wire.QetAutoRouteDiagnosticsJson) + payload = json.loads(wire.QetRouteDiagnosticsJson) - self.assertGreater(float(wire.QetAutoRouteLengthMm), 0.0) + self.assertGreater(float(wire.QetRouteLengthMm), 0.0) self.assertEqual("42", wire.QetWireStyleId) self.assertEqual("42", payload["wire_style_id"]) self.assertGreater(payload["length_mm"], 0.0) + self.assertTrue(payload["route_track"]["segments"]) + self.assertEqual("WireDuct", payload["route_track"]["segments"][0]["carrier"]["kind"]) + self.assertTrue(json.loads(wire.QetRouteTrackJson)["carrier_names"]) - def test_network_auto_route_offsets_lane_by_route_index(self): + def test_network_eplan_connection_route_offsets_lane_by_route_index(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -317,7 +296,7 @@ class AutoRoutingTest(unittest.TestCase): kind="WireDuct", ) - first = auto_routing.route_between_terminals( + first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, @@ -325,7 +304,7 @@ class AutoRoutingTest(unittest.TestCase): wire_uuid="wire-1", options={"lane_spacing": 12.0, "lane_axis": "y"}, ) - second = auto_routing.route_between_terminals( + second = auto_routing.route_eplan_connection_between_terminals( doc, start, end, @@ -333,7 +312,7 @@ class AutoRoutingTest(unittest.TestCase): wire_uuid="wire-2", options={"lane_spacing": 12.0, "lane_axis": "y"}, ) - payload = json.loads(second["wire"].QetAutoRouteDiagnosticsJson) + payload = json.loads(second["wire"].QetRouteDiagnosticsJson) self.assertTrue(any(abs(point.y - 0.0) <= 0.001 for point in first["points"][1:-1])) self.assertTrue(any(abs(point.y - 12.0) <= 0.001 for point in second["points"][1:-1])) @@ -341,7 +320,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("y", payload["lane"]["axis"]) self.assertEqual(12.0, payload["lane"]["offset_mm"]) - def test_auto_route_replaces_existing_wire_uuid_when_endpoints_change(self): + def test_eplan_connection_route_replaces_existing_wire_uuid_when_endpoints_change(self): _install_fake_freecad() terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -363,8 +342,8 @@ class AutoRoutingTest(unittest.TestCase): kind="WireDuct", ) - auto_routing.route_between_terminals(doc, start_old, end_old, wire_uuid="wire-1") - auto_routing.route_between_terminals(doc, start_new, end_new, wire_uuid="wire-1") + auto_routing.route_eplan_connection_between_terminals(doc, start_old, end_old, wire_uuid="wire-1") + auto_routing.route_eplan_connection_between_terminals(doc, start_new, end_new, wire_uuid="wire-1") routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual(1, len(routed_wires)) @@ -426,7 +405,7 @@ class AutoRoutingTest(unittest.TestCase): ) network = routing_network.build_route_graph(doc) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual(5, len(network["nodes"])) self.assertEqual("network-dijkstra-v1", result["algorithm"]) @@ -454,14 +433,42 @@ class AutoRoutingTest(unittest.TestCase): ) network = routing_network.build_route_graph(doc) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertIn((40.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) self.assertIn((80.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) self.assertGreaterEqual(network["segment_count"], 3) - def test_auto_route_prefers_wire_duct_over_auxiliary_range(self): + def test_route_graph_bridges_adjoining_wire_duct_gap_with_eplan_tolerance(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(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + network = routing_network.build_route_graph(doc) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual(1, network["bridged_segment_count"]) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertEqual("Routed", result["route_status"]) + + def test_eplan_connection_route_prefers_wire_duct_over_auxiliary_range(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -487,7 +494,7 @@ class AutoRoutingTest(unittest.TestCase): kind="WireDuct", ) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertTrue(any(point.y == 40.0 for point in result["points"])) @@ -513,7 +520,7 @@ class AutoRoutingTest(unittest.TestCase): offset=5.0, margin=0.0, ) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertGreater(len(created), 0) self.assertEqual("RoutingRange", getattr(created[0], "QetRouteCarrierKind", "")) @@ -558,7 +565,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertFalse(hasattr(cabinet, "QetRoutingSourceKind")) self.assertFalse(hasattr(duct, "QetRoutingSourceKind")) - def test_auto_route_can_use_auto_detected_support_surface(self): + def test_eplan_connection_route_can_use_auto_detected_support_surface(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -577,7 +584,7 @@ class AutoRoutingTest(unittest.TestCase): offset=5.0, margin=0.0, ) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertGreater(len(created), 0) self.assertEqual("network-dijkstra-v1", result["algorithm"]) @@ -585,7 +592,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(0, result["collision_count"]) self.assertTrue(any(point.y == 10.0 for point in result["points"])) - def test_generate_layout_space_auto_detects_support_surface(self): + def test_prepare_layout_space_auto_detects_support_surface_sources(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -599,7 +606,7 @@ class AutoRoutingTest(unittest.TestCase): result = auto_routing_panel.AutoRoutingController().generate_layout_space() - self.assertGreater(result["surface_carriers"], 0) + self.assertGreater(result["support_surface_sources"], 0) self.assertEqual("document", result["source_mode"]) def test_generate_routing_paths_uses_selected_wire_duct_entity(self): @@ -623,7 +630,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, result["wire_duct_carriers"]) self.assertEqual("selection", result["source_mode"]) - def test_generate_layout_space_uses_whole_document_not_selected_face_workflow(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() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -642,10 +649,10 @@ class AutoRoutingTest(unittest.TestCase): result = auto_routing_panel.AutoRoutingController().generate_layout_space() - self.assertGreater(result["surface_carriers"], 0) + self.assertGreater(result["support_surface_sources"], 0) self.assertEqual("document", result["source_mode"]) - def test_generate_layout_space_adds_terminal_access_to_route_network(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() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -659,8 +666,8 @@ class AutoRoutingTest(unittest.TestCase): duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - result = auto_routing_panel.AutoRoutingController().generate_layout_space() - result_again = auto_routing_panel.AutoRoutingController().generate_layout_space() + 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) @@ -668,13 +675,86 @@ class AutoRoutingTest(unittest.TestCase): ] 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_layout_space_skips_far_terminal_access_to_protect_view_bbox(self): + 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() + 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, "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_generate_routing_path_network_adds_wiring_cut_out_carrier(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") + 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_panel.AutoRoutingController().generate_routing_paths() + cut_out_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" + ] + + 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_writes_diagnostic_for_unconnected_terminal(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") + _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) + + self.assertFalse(result["ok"]) + self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) + self.assertEqual(1, len(payload["unconnected_terminals"])) + self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"]) + + 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") @@ -687,12 +767,13 @@ class AutoRoutingTest(unittest.TestCase): duct.Label = "Wire Duct Far" duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) - result = auto_routing_panel.AutoRoutingController().generate_layout_space() + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() 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_route_all_prepares_layout_space_like_one_click_routing(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() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -716,10 +797,13 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing_panel.AutoRoutingController().route_all() + report = auto_routing_panel.AutoRoutingController().route_eplan_connections() 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) @@ -728,7 +812,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, diagnostic_payload["prepared_layout"]["wire_duct_carriers"]) self.assertEqual(2, diagnostic_payload["prepared_layout"]["terminal_access_carriers"]) - def test_auto_route_rejects_far_network_entry_to_avoid_huge_render_bbox(self): + 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"] @@ -744,9 +828,9 @@ class AutoRoutingTest(unittest.TestCase): ) with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_between_terminals(doc, start, end) + auto_routing.route_eplan_connection_between_terminals(doc, start, end) - def test_route_between_terminals_fails_without_network(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() app = sys.modules["FreeCAD"] @@ -757,7 +841,7 @@ class AutoRoutingTest(unittest.TestCase): end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 30, 0)) with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_between_terminals(doc, start, end) + auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual(0, len(wiring_objects.iter_routed_wire_objects(doc))) def test_surface_carrier_grid_uses_actual_rotated_face_plane(self): @@ -861,9 +945,11 @@ class AutoRoutingTest(unittest.TestCase): margin=20.0, ) - self.assertEqual(1, len(created)) - carrier = created[0] + 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]) @@ -888,9 +974,10 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", ) - self.assertEqual(1, len(created)) + self.assertEqual(3, len(created)) self.assertEqual(0, len(created_again)) - self.assertEqual("WireDuct", created[0].QetRouteCarrierKind) + 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")) @@ -911,13 +998,13 @@ class AutoRoutingTest(unittest.TestCase): margin=0.0, ) - result = auto_routing.route_between_terminals(doc, start, end) + 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"]) - def test_auto_route_uses_alternate_carrier_to_avoid_obstacle(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"] @@ -945,7 +1032,7 @@ class AutoRoutingTest(unittest.TestCase): obstacle = doc.addObject("Part::Feature", "CabinetObstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 15, 25)) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) @@ -954,15 +1041,15 @@ class AutoRoutingTest(unittest.TestCase): self.assertGreaterEqual(result["network"]["blocked_segments"], 1) self.assertIn(50.0, [point.y for point in result["points"]]) - def test_auto_route_marks_collision_warning_against_obstacle_bbox(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() + 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( + routing_network.create_route_carrier( doc, [app.Vector(0, 0, 100), app.Vector(100, 0, 100)], project_uuid="project-1", @@ -970,13 +1057,13 @@ class AutoRoutingTest(unittest.TestCase): obstacle = doc.addObject("Part::Feature", "Obstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) - result = auto_routing.route_between_terminals(doc, start, end) + 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"]) - def test_auto_route_ignores_terminal_exit_segment_collision(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"] @@ -992,12 +1079,12 @@ class AutoRoutingTest(unittest.TestCase): terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) - def test_auto_route_ignores_endpoint_device_body_as_obstacle(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() app = sys.modules["FreeCAD"] @@ -1017,12 +1104,12 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", ) - result = auto_routing.route_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) - def test_route_all_from_payload_skips_missing_terminal(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() app = sys.modules["FreeCAD"] @@ -1039,7 +1126,7 @@ class AutoRoutingTest(unittest.TestCase): ] } - report = auto_routing.route_all_from_payload(doc, payload) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["skipped_missing_terminal"]) @@ -1050,7 +1137,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertTrue(report["missing_endpoint_samples"][0]["start_found"]) self.assertFalse(report["missing_endpoint_samples"][0]["end_found"]) - def test_route_all_writes_diagnostic_object_for_missing_terminal(self): + 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"] @@ -1068,17 +1155,17 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_all_from_payload(doc, payload) + 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("AutoRouteBatch", diagnostic.QetDiagnosticKind) + self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) - def test_route_all_reports_total_auto_route_length(self): + def test_route_eplan_connections_reports_total_connection_route_length(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -1103,14 +1190,14 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_all_from_payload(doc, payload) - message = auto_routing.format_route_all_report(report) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) self.assertGreater(report["total_length_mm"], 0.0) self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"]) self.assertIn("总长度", message) - def test_route_all_report_keeps_route_identity_and_diagnostics(self): + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -1137,7 +1224,7 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_all_from_payload( + report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"lane_spacing": 12.0, "lane_axis": "y"}, @@ -1152,8 +1239,9 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(0, route["lane"]["index"]) self.assertEqual("network-dijkstra-v1", route["algorithm"]) self.assertEqual(1, route["network"]["carriers"]) + self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) - def test_route_all_report_includes_collision_samples(self): + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -1181,8 +1269,8 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_all_from_payload(doc, payload) - message = auto_routing.format_route_all_report(report) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["collision_warnings"]) self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) @@ -1192,7 +1280,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("碰撞示例", message) self.assertIn("Middle Obstacle", message) - def test_route_all_report_calls_out_local_unbound_terminals(self): + def test_route_eplan_connections_report_calls_out_local_unbound_terminals(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -1215,8 +1303,8 @@ class AutoRoutingTest(unittest.TestCase): ] } - report = auto_routing.route_all_from_payload(doc, payload) - message = auto_routing.format_route_all_report(report) + 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["available_terminals"]) @@ -1224,7 +1312,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("端子匹配失败", message) self.assertIn("local:", message) - def test_route_all_report_includes_network_and_first_error(self): + def test_route_eplan_connections_report_includes_network_and_first_error(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { @@ -1246,7 +1334,7 @@ class AutoRoutingTest(unittest.TestCase): "errors": ["没有可用的线槽/路由路径网络"], } - message = auto_routing.format_route_all_report(report) + message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("routed=1", message) self.assertIn("线槽路径 2 条", message) @@ -1318,9 +1406,9 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual("qet", indexed["qet-terminal-p1"].QetTerminalBindingMode) - def test_route_all_rebinds_local_template_terminals_from_wire_endpoints(self): + 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() + 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") @@ -1357,6 +1445,12 @@ class AutoRoutingTest(unittest.TestCase): slot_name=slot_name, ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + ) + payload = { "project_uuid": "project-1", "wires": [ @@ -1374,10 +1468,9 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_all_from_payload( + report = auto_routing.route_eplan_connections_from_payload( doc, payload, - options={"allow_floating_fallback": True}, ) indexed = auto_routing.index_terminals(doc) @@ -1401,7 +1494,7 @@ class AutoRoutingTest(unittest.TestCase): [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", ) - wire = auto_routing.route_between_terminals(doc, start, end)["wire"] + wire = auto_routing.route_eplan_connection_between_terminals(doc, start, end)["wire"] removed = routing_network.clear_route_carriers(doc) From 10401497f315b8da387f4911448459a885ac001c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Fri, 29 May 2026 17:54:36 +0800 Subject: [PATCH 03/63] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0FreeCAD?= =?UTF-8?q?=E9=9D=A2=E8=B4=B4=E5=90=88=E8=A3=85=E9=85=8D=E8=BE=85=E5=8A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-05-29-face-contact-snap-design.md | 47 +++ src/Mod/FreeCADExchange/CMakeLists.txt | 20 + src/Mod/FreeCADExchange/DeviceImport.py | 41 +- src/Mod/FreeCADExchange/ManualWiringPanel.py | 394 +++++++++++++++--- ...eecad_exchange_device_import_fcstd_test.py | 26 ++ ...eecad_exchange_manual_wiring_panel_test.py | 164 ++++++++ 6 files changed, 619 insertions(+), 73 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-29-face-contact-snap-design.md diff --git a/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md b/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md new file mode 100644 index 0000000..19bbbd3 --- /dev/null +++ b/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md @@ -0,0 +1,47 @@ +# 面贴合装配辅助设计 + +## 背景 + +CAD 用户在 FreeCAD 中摆放导轨、线槽和设备时,需要让两个接触面刚好贴合,避免穿模或悬空。FreeCAD 原生 `变换` 可以移动旋转对象,但不会自动判断面接触;`Assembly` 工作台可以做装配约束,但对当前 QET 演示流程偏重。 + +## 目标 + +在 `QET模板 -> 3D手动布线` 面板增加一个轻量装配辅助按钮:`贴合到选中面`。 + +用户先选择目标承载面,再选择要移动对象的接触面,点击按钮后: + +- 沿第一个目标面的法向移动第二个对象,使第二个选择面落到目标面的同一平面上。 +- 尽量让第二个选择面的法向与第一个选择面的反向对齐。 +- 保持第二个对象原来的切向位置,不把对象横向拉到目标面的拾取点。 +- 操作完成后恢复当前 QET 工程为活动文档。 +- 不写数据库,不改 2D/3D 绑定表,不影响导线任务。 + +## 适用场景 + +- 导轨背面贴合机柜安装板。 +- 线槽背面或底面贴合机柜安装板。 +- 设备背面或卡扣接触面贴合导轨安装面。 + +## 交互 + +1. 在 3D 视图中选择机柜、导轨或线槽上的目标面。 +2. 按住 Ctrl,再选择要移动对象上的接触面。 +3. 点击 `贴合到选中面`。 +4. 如果方向不理想,用户可以先用 FreeCAD `变换` 粗调姿态,再重新执行贴合。 + +第一版只接受两个面。多选多个设备、多个面时,系统不能唯一判断哪个对象应该移动、哪个面是目标、是否要同时满足多个约束,因此会直接提示错误。多面贴合属于完整 Assembly 约束求解范围,不放进这个轻量按钮。 + +第一版不做多约束求解,不自动识别“哪个面是背面”,也不保存永久装配约束。它只执行一次几何位姿调整。 + +## 错误处理 + +- 少于两个面:提示用户先选目标面,再选移动对象接触面。 +- 多于两个面:提示只能选择两个面。 +- 第二个选择对象没有可移动 `Placement`:提示对象不能移动。 +- 无法读取面中心或法向:提示请选择有效模型面。 + +## 测试 + +- 选择两个面后,移动对象应只沿目标面法向平移,消除法向间距。 +- 多选三个或更多面时应报错。 +- 导入类操作或贴合操作后,`App.ActiveDocument` 仍应是当前 QET 工程。 diff --git a/src/Mod/FreeCADExchange/CMakeLists.txt b/src/Mod/FreeCADExchange/CMakeLists.txt index 020ba62..f1520fa 100644 --- a/src/Mod/FreeCADExchange/CMakeLists.txt +++ b/src/Mod/FreeCADExchange/CMakeLists.txt @@ -23,6 +23,19 @@ set(FreeCADExchange_Scripts ManualWiringPanel.py ) +set(FreeCADExchange_CabinetAssetDir + ${CMAKE_CURRENT_SOURCE_DIR}/../../../data/examples/qet_cabinet_assets +) + +set(FreeCADExchange_CabinetAssets + ${FreeCADExchange_CabinetAssetDir}/README.md + ${FreeCADExchange_CabinetAssetDir}/qet_cabinet_assets_report.json + ${FreeCADExchange_CabinetAssetDir}/qet_din_rail.FCStd + ${FreeCADExchange_CabinetAssetDir}/qet_din_rail.step + ${FreeCADExchange_CabinetAssetDir}/qet_wire_duct.FCStd + ${FreeCADExchange_CabinetAssetDir}/qet_wire_duct.step +) + add_custom_target(FreeCADExchangeScripts ALL SOURCES ${FreeCADExchange_Scripts} ) @@ -39,3 +52,10 @@ install( DESTINATION Mod/FreeCADExchange ) + +install( + FILES + ${FreeCADExchange_CabinetAssets} + DESTINATION + data/examples/qet_cabinet_assets +) diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index d4fc078..646c3e3 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -791,25 +791,28 @@ def _import_model_into_group(doc, device_group, model_path, merge=False, use_lin before_names = _existing_object_names(doc) try: - ImportGui.insert( - name=model_path, - docName=doc.Name, - merge=bool(merge), - useLinkGroup=bool(use_link_group), - ) - except Exception: - for obj in _new_objects_since(doc, before_names): - _remove_object_tree(doc, obj) - raise - - imported_objects = _new_objects_since(doc, before_names) - top_level_objects = _top_level_imported_objects(imported_objects) - for obj in top_level_objects: - if obj not in getattr(device_group, "Group", []): - device_group.addObject(obj) - TemplateSemantics.clear_stored_template_slot_hints(device_group) - TerminalObjects.hide_template_terminal_hints(device_group) - return top_level_objects + try: + ImportGui.insert( + name=model_path, + docName=doc.Name, + merge=bool(merge), + useLinkGroup=bool(use_link_group), + ) + except Exception: + for obj in _new_objects_since(doc, before_names): + _remove_object_tree(doc, obj) + raise + + imported_objects = _new_objects_since(doc, before_names) + top_level_objects = _top_level_imported_objects(imported_objects) + for obj in top_level_objects: + if obj not in getattr(device_group, "Group", []): + device_group.addObject(obj) + TemplateSemantics.clear_stored_template_slot_hints(device_group) + TerminalObjects.hide_template_terminal_hints(device_group) + return top_level_objects + finally: + _activate_document(doc) def _open_fcstd_source_document(model_path): diff --git a/src/Mod/FreeCADExchange/ManualWiringPanel.py b/src/Mod/FreeCADExchange/ManualWiringPanel.py index 55c7634..3da71e3 100644 --- a/src/Mod/FreeCADExchange/ManualWiringPanel.py +++ b/src/Mod/FreeCADExchange/ManualWiringPanel.py @@ -1,6 +1,7 @@ # FreeCADExchange GUI panel for guided manual 3D wiring. import json +import math from pathlib import Path import FreeCAD as App @@ -68,15 +69,32 @@ def _console_error(message): pass -def _repo_root(): - return Path(__file__).resolve().parents[3] - - def _builtin_carrier_asset_path(carrier_kind): file_name = BUILTIN_CARRIER_ASSETS.get((carrier_kind or "").strip()) if not file_name: return "" - return str(_repo_root() / "data" / "examples" / "qet_cabinet_assets" / file_name) + module_path = Path(__file__).resolve() + candidate_roots = [] + for index in (3, 2): + try: + candidate_roots.append(module_path.parents[index]) + except IndexError: + pass + try: + app_home = App.ConfigGet("AppHomePath") + if app_home: + candidate_roots.append(Path(app_home)) + except Exception: + pass + + for root in candidate_roots: + candidate = root / "data" / "examples" / "qet_cabinet_assets" / file_name + if candidate.is_file(): + return str(candidate) + + if candidate_roots: + return str(candidate_roots[0] / "data" / "examples" / "qet_cabinet_assets" / file_name) + return str(module_path.parent / file_name) def _supported_carrier_asset(path): @@ -91,6 +109,29 @@ def _active_document(): return doc +def _activate_document(doc): + if doc is None: + return + + setter = getattr(App, "setActiveDocument", None) + if callable(setter): + try: + setter(doc.Name) + except Exception: + pass + + try: + App.ActiveDocument = doc + except Exception: + pass + + try: + if Gui is not None: + Gui.ActiveDocument = Gui.getDocument(doc.Name) + except Exception: + pass + + def _selection(): if Gui is None: return [] @@ -120,6 +161,195 @@ def _shape_center(shape): ) +def _vector(x=0.0, y=0.0, z=0.0): + return App.Vector(float(x), float(y), float(z)) + + +def _vector_add(left, right): + return _vector(left.x + right.x, left.y + right.y, left.z + right.z) + + +def _vector_sub(left, right): + return _vector(left.x - right.x, left.y - right.y, left.z - right.z) + + +def _vector_scale(vector, factor): + return _vector(vector.x * float(factor), vector.y * float(factor), vector.z * float(factor)) + + +def _vector_length(vector): + return math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z) + + +def _normalize_vector(vector): + if vector is None: + return None + length = _vector_length(vector) + if length <= 1e-9: + return None + return _vector_scale(vector, 1.0 / length) + + +def _vector_dot(left, right): + return left.x * right.x + left.y * right.y + left.z * right.z + + +def _vectors_close(left, right, tolerance=1e-6): + return _vector_length(_vector_sub(left, right)) <= tolerance + + +def _face_anchor_point(picked, sub_object): + picked_points = list(getattr(picked, "PickedPoints", []) or []) + if picked_points: + return picked_points[0] + center = getattr(sub_object, "CenterOfMass", None) + if center is not None: + return center + return _shape_center(sub_object) + + +def _face_normal(sub_object): + if not hasattr(sub_object, "normalAt"): + return None + try: + return _normalize_vector(sub_object.normalAt(0.5, 0.5)) + except Exception: + try: + return _normalize_vector(sub_object.normalAt(0.5)) + except Exception: + return None + + +def _selected_contact_face_refs(): + refs = [] + for picked in _selection_ex(): + obj = getattr(picked, "Object", None) + if obj is None: + continue + subelement_names = list(getattr(picked, "SubElementNames", []) or []) + for index, sub_object in enumerate(list(getattr(picked, "SubObjects", []) or [])): + shape_type = (getattr(sub_object, "ShapeType", "") or "").strip().lower() + if shape_type != "face": + continue + point = _face_anchor_point(picked, sub_object) + normal = _face_normal(sub_object) + if point is None or normal is None: + continue + refs.append( + { + "object": obj, + "face": sub_object, + "point": point, + "normal": normal, + "subelement_name": subelement_names[index] if index < len(subelement_names) else "", + } + ) + break + return refs + + +def _has_placement(obj): + return getattr(obj, "Placement", None) is not None + + +def _contact_transform_object(obj): + carrier = _carrier_object_from_object(obj) + if carrier is not None and _has_placement(carrier): + return carrier + + best = obj if _has_placement(obj) else None + current = obj + visited = set() + while current is not None and id(current) not in visited: + visited.add(id(current)) + parents = list(getattr(current, "InList", []) or []) + parent = parents[0] if parents else None + if parent is None: + break + if _has_placement(parent): + name = getattr(parent, "Name", "") or "" + if ( + name.startswith("QETDevice_") + or (getattr(parent, "QetInstanceId", "") or "").strip() + or (getattr(parent, "QetCarrierKind", "") or "").strip() + ): + best = parent + current = parent + return best + + +def _rotation_for_face_contact(moving_normal, target_normal): + desired = _vector_scale(target_normal, -1.0) + moving = _normalize_vector(moving_normal) + desired = _normalize_vector(desired) + if moving is None or desired is None or _vectors_close(moving, desired): + return None + try: + return App.Rotation(moving, desired) + except Exception: + return None + + +def _normal_contact_translation(target_point, target_normal, moving_point): + normal = _normalize_vector(target_normal) + if normal is None: + return None + signed_distance = _vector_dot(_vector_sub(moving_point, target_point), normal) + return _vector_scale(normal, -signed_distance) + + +def _rotate_object_about_point(obj, rotation, pivot): + if rotation is None: + return False + placement = getattr(obj, "Placement", None) + if placement is None or not hasattr(rotation, "multVec"): + return False + + base = getattr(placement, "Base", None) + if base is None: + return False + + try: + rotated_base = _vector_add(pivot, rotation.multVec(_vector_sub(base, pivot))) + except Exception: + return False + + old_rotation = getattr(placement, "Rotation", None) + new_rotation = old_rotation + try: + if hasattr(rotation, "multiply"): + new_rotation = rotation.multiply(old_rotation) + except Exception: + new_rotation = old_rotation + + try: + obj.Placement = App.Placement(rotated_base, new_rotation) + return True + except Exception: + try: + placement.Base = rotated_base + placement.Rotation = new_rotation + obj.Placement = placement + return True + except Exception: + return False + + +def _translate_object(obj, translation): + placement = getattr(obj, "Placement", None) + base = getattr(placement, "Base", None) + if placement is None or base is None: + raise ManualWiringPanelError("所选对象没有可移动的 Placement,不能执行贴合。") + + new_base = _vector_add(base, translation) + try: + placement.Base = new_base + obj.Placement = placement + except Exception: + obj.Placement = App.Placement(new_base, getattr(placement, "Rotation", App.Rotation())) + return new_base + + def _selected_point(): for picked in _selection_ex(): picked_points = list(getattr(picked, "PickedPoints", []) or []) @@ -377,32 +607,36 @@ def _import_carrier_objects_from_path(doc, path): if not _supported_carrier_asset(path): raise ManualWiringPanelError("请选择 STEP/STP/FCStd 线槽或导轨模型。") - suffix = Path(path).suffix.lower() - if suffix == ".fcstd": - source_doc = None - try: - source_doc = _open_fcstd_source(path) - copied = [] - for source_obj in _top_level_objects(list(getattr(source_doc, "Objects", []) or [])): - copied.append(doc.copyObject(source_obj, True)) - return copied - finally: - if source_doc is not None: - try: - App.closeDocument(source_doc.Name) - except Exception: - pass - - if ImportGui is None: - raise ManualWiringPanelError("当前 FreeCAD 无法导入 STEP/STP 文件。") - before = _existing_object_names(doc) - ImportGui.insert( - name=path, - docName=doc.Name, - merge=False, - useLinkGroup=True, - ) - return _top_level_objects(_new_objects_since(doc, before)) + try: + suffix = Path(path).suffix.lower() + if suffix == ".fcstd": + source_doc = None + try: + source_doc = _open_fcstd_source(path) + copied = [] + for source_obj in _top_level_objects(list(getattr(source_doc, "Objects", []) or [])): + copied.append(doc.copyObject(source_obj, True)) + return copied + finally: + if source_doc is not None: + try: + App.closeDocument(source_doc.Name) + except Exception: + pass + + if ImportGui is None: + raise ManualWiringPanelError("当前 FreeCAD 无法导入 STEP/STP 文件。") + before = _existing_object_names(doc) + ImportGui.insert( + name=path, + docName=doc.Name, + merge=False, + useLinkGroup=True, + ) + return _top_level_objects(_new_objects_since(doc, before)) + finally: + _activate_document(doc) + def _ensure_float_property(obj, prop_name, value, description): @@ -776,30 +1010,33 @@ class ManualWiringController: raise ManualWiringPanelError("找不到内置线槽/导轨资产。") doc = _active_document() - project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() - carrier_group = WiringObjects.ensure_carrier_group(doc, project_uuid) - imported = list((importer or _import_carrier_objects_from_path)(doc, path) or []) - if not imported: - raise ManualWiringPanelError("没有从模型文件导入任何对象。") - - role_label = _carrier_role_label(carrier_kind) - container_name = "QETCarrier_{0}".format(TerminalObjects.safe_token(carrier_kind)) - container = doc.addObject("App::DocumentObjectGroup", container_name) - container.Label = "QET {0}".format(role_label or carrier_kind) - carrier_group.addObject(container) - for obj in imported: - if obj not in getattr(container, "Group", []): - container.addObject(obj) - - _set_carrier_properties(container, carrier_kind, source_path=path) - for obj in imported: - _set_carrier_properties(obj, carrier_kind, source_path=path) - _apply_carrier_length(container, length_mm) try: - doc.recompute() - except Exception: - pass - return container + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + carrier_group = WiringObjects.ensure_carrier_group(doc, project_uuid) + imported = list((importer or _import_carrier_objects_from_path)(doc, path) or []) + if not imported: + raise ManualWiringPanelError("没有从模型文件导入任何对象。") + + role_label = _carrier_role_label(carrier_kind) + container_name = "QETCarrier_{0}".format(TerminalObjects.safe_token(carrier_kind)) + container = doc.addObject("App::DocumentObjectGroup", container_name) + container.Label = "QET {0}".format(role_label or carrier_kind) + carrier_group.addObject(container) + for obj in imported: + if obj not in getattr(container, "Group", []): + container.addObject(obj) + + _set_carrier_properties(container, carrier_kind, source_path=path) + for obj in imported: + _set_carrier_properties(obj, carrier_kind, source_path=path) + _apply_carrier_length(container, length_mm) + try: + doc.recompute() + except Exception: + pass + return container + finally: + _activate_document(doc) def apply_length_to_selected_carriers(self, length_mm): selected = _selected_carrier_objects() @@ -818,6 +1055,44 @@ class ManualWiringController: pass return updated + def align_selected_contact_faces(self): + refs = _selected_contact_face_refs() + if len(refs) < 2: + raise ManualWiringPanelError("请先选择目标面,再按 Ctrl 选择要移动对象的接触面。") + if len(refs) > 2: + raise ManualWiringPanelError("只能选择两个面:第一个是目标面,第二个是要移动对象的接触面。") + + target = refs[0] + moving = refs[1] + moving_object = _contact_transform_object(moving["object"]) + if moving_object is None: + raise ManualWiringPanelError("没有找到可移动的对象。") + + rotation = _rotation_for_face_contact(moving["normal"], target["normal"]) + rotated = _rotate_object_about_point(moving_object, rotation, moving["point"]) + translation = _normal_contact_translation( + target["point"], + target["normal"], + moving["point"], + ) + if translation is None: + raise ManualWiringPanelError("无法读取目标面的法向,不能执行贴合。") + _translate_object(moving_object, translation) + try: + _active_document().recompute() + except Exception: + pass + _activate_document(_active_document()) + return { + "target_object": target["object"], + "moving_object": moving_object, + "target_point": target["point"], + "moving_point": moving["point"], + "translation": translation, + "translation_mode": "normal", + "rotated": rotated, + } + def _clear_preview_objects(self): doc = getattr(App, "ActiveDocument", None) if doc is None: @@ -1108,6 +1383,7 @@ class ManualWiringTaskPanel: self.mark_duct_button = QtWidgets.QPushButton("标记为线槽") self.mark_cabinet_button = QtWidgets.QPushButton("标记为柜面") self.mark_rail_button = QtWidgets.QPushButton("标记为导轨") + self.align_faces_button = QtWidgets.QPushButton("贴合到选中面") self.waypoint_button = QtWidgets.QPushButton("添加折点") self.delete_waypoint_button = QtWidgets.QPushButton("删除最后折点") self.end_button = QtWidgets.QPushButton("设为终点并生成") @@ -1140,6 +1416,7 @@ class ManualWiringTaskPanel: carrier_layout.addWidget(self.mark_cabinet_button) carrier_layout.addWidget(self.mark_rail_button) layout.addLayout(carrier_layout) + layout.addWidget(self.align_faces_button) layout.addWidget(self.start_button) layout.addWidget(self.waypoint_button) layout.addWidget(self.delete_waypoint_button) @@ -1171,6 +1448,7 @@ class ManualWiringTaskPanel: self.mark_duct_button.clicked.connect(self.mark_wire_duct) self.mark_cabinet_button.clicked.connect(self.mark_cabinet) self.mark_rail_button.clicked.connect(self.mark_rail) + self.align_faces_button.clicked.connect(self.align_selected_contact_faces) self.start_button.clicked.connect(self.set_start) self.waypoint_button.clicked.connect(self.add_waypoint) self.delete_waypoint_button.clicked.connect(self.delete_last_waypoint) @@ -1323,6 +1601,14 @@ class ManualWiringTaskPanel: except Exception as exc: self._set_error(str(exc)) + def align_selected_contact_faces(self): + try: + result = self.controller.align_selected_contact_faces() + moving_label = getattr(result.get("moving_object"), "Label", "") or getattr(result.get("moving_object"), "Name", "") + self._set_status("已贴合对象:{0}".format(moving_label or "选中对象")) + except Exception as exc: + self._set_error(str(exc)) + def set_start(self): try: terminal = self.controller.set_start_from_selection() diff --git a/tests/python/freecad_exchange_device_import_fcstd_test.py b/tests/python/freecad_exchange_device_import_fcstd_test.py index 5e53b91..ec567d5 100644 --- a/tests/python/freecad_exchange_device_import_fcstd_test.py +++ b/tests/python/freecad_exchange_device_import_fcstd_test.py @@ -486,6 +486,32 @@ class FcstdDeviceImportTest(unittest.TestCase): self.assertEqual("", device_group.QetTemplateSlotsJson) self.assertEqual([], template_semantics.collect_terminal_hints(device_group)) + def test_non_fcstd_import_restores_target_document_after_insert_changes_active_document(self): + source = FakeDocument("Source", r"D:\models\breaker.FCStd") + _install_fake_freecad(source) + app = sys.modules["FreeCAD"] + + doc = FakeDocument("QETScene") + app.ActiveDocument = doc + device_group = doc.addObject("App::Part", "QETDevice_breaker") + + device_import, _ = _reload_modules() + + def insert_step_body(name, docName, merge, useLinkGroup): + doc.addObject("Part::Feature", "StepBody") + app.ActiveDocument = None + + device_import.ImportGui.insert = insert_step_body + + device_import._import_model_into_group( + doc, + device_group, + r"D:\models\breaker.step", + ) + + self.assertIs(doc, app.ActiveDocument) + self.assertIn("QETScene", app.set_active_document_calls) + def test_fcstd_import_detaches_removed_template_lcs_from_parent_group(self): source = FakeDocument("Source", r"D:\models\breaker.FCStd") _install_fake_freecad(source) diff --git a/tests/python/freecad_exchange_manual_wiring_panel_test.py b/tests/python/freecad_exchange_manual_wiring_panel_test.py index 6fde382..3c1e003 100644 --- a/tests/python/freecad_exchange_manual_wiring_panel_test.py +++ b/tests/python/freecad_exchange_manual_wiring_panel_test.py @@ -1,6 +1,7 @@ import importlib import json import sys +import tempfile import types import unittest from pathlib import Path @@ -198,6 +199,29 @@ def _reload_modules(): class ManualWiringPanelTest(unittest.TestCase): + def test_builtin_carrier_asset_path_supports_installed_freecad_layout(self): + _selection_state = _install_fake_freecad() + _terminal_objects, panel = _reload_modules() + + with tempfile.TemporaryDirectory() as temp_dir: + app_home = Path(temp_dir) / "run-FreeCAD" + module_dir = app_home / "Mod" / "FreeCADExchange" + asset_dir = app_home / "data" / "examples" / "qet_cabinet_assets" + module_dir.mkdir(parents=True) + asset_dir.mkdir(parents=True) + asset = asset_dir / "qet_din_rail.FCStd" + asset.write_text("fake rail", encoding="utf-8") + + original_file = panel.__file__ + try: + panel.__file__ = str(module_dir / "ManualWiringPanel.py") + + resolved = panel._builtin_carrier_asset_path("rail") + finally: + panel.__file__ = original_file + + self.assertEqual(str(asset), resolved) + def test_controller_rejects_local_terminal_as_manual_wiring_start(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() @@ -415,6 +439,29 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertIn(carrier, carrier_group.Group) self.assertEqual(3.0, getattr(carrier, "QetCarrierScaleX", None)) + def test_controller_restores_active_document_after_carrier_import_changes_it(self): + _selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + + def importer(doc, path): + obj = doc.addObject("Part::Feature", "ImportedRail") + app.ActiveDocument = None + return [obj] + + panel.ManualWiringController().import_carrier_asset( + r"D:\assets\rail.FCStd", + "rail", + length_mm=300.0, + importer=importer, + ) + + self.assertIs(doc, app.ActiveDocument) + def test_controller_applies_length_to_selected_carrier(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() @@ -441,6 +488,123 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual(500.0, getattr(carrier, "QetCarrierLength", None)) self.assertEqual(2.5, getattr(carrier, "QetCarrierScaleX", None)) + def test_controller_aligns_second_selected_face_to_first_selected_face(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(5, 6, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=rail, + ), + ] + + result = panel.ManualWiringController().align_selected_contact_faces() + + self.assertIs(rail, result["moving_object"]) + self.assertEqual( + (0.0, 0.0, 1.0), + (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z), + ) + self.assertEqual( + (-0.0, -0.0, -9.0), + ( + result["translation"].x, + result["translation"].y, + result["translation"].z, + ), + ) + self.assertEqual("normal", result["translation_mode"]) + + def test_controller_requires_two_faces_for_contact_alignment(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[face], + SubElementNames=["Face1"], + Object=cabinet, + ) + ] + + with self.assertRaisesRegex(panel.ManualWiringPanelError, "目标面"): + panel.ManualWiringController().align_selected_contact_faces() + + def test_controller_rejects_more_than_two_faces_for_contact_alignment(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + breaker = doc.addObject("Part::Feature", "Breaker") + + face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 0)], + SubObjects=[face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 10)], + SubObjects=[face], + SubElementNames=["Face2"], + Object=rail, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 20)], + SubObjects=[face], + SubElementNames=["Face3"], + Object=breaker, + ), + ] + + with self.assertRaisesRegex(panel.ManualWiringPanelError, "只能选择两个面"): + panel.ManualWiringController().align_selected_contact_faces() + def test_controller_deletes_last_waypoint_and_preview_point(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() From e6ab63be4f214f6287e1d20a3dbf06c265b596f4 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Fri, 29 May 2026 18:11:20 +0800 Subject: [PATCH 04/63] =?UTF-8?q?fix:=20=E6=8C=89=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=E5=AF=B9=E5=88=86=E9=85=8D=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF?= =?UTF-8?q?=E5=B9=B6=E7=BA=BF=E5=81=8F=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 20 ++++++- .../freecad_exchange_auto_routing_test.py | 53 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 144a272..e259a9a 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1098,6 +1098,18 @@ def _wire_item_value(item, *names): return "" +def _route_lane_key(start_uuid, end_uuid): + endpoints = sorted( + value + for value in ( + str(start_uuid or "").strip(), + str(end_uuid or "").strip(), + ) + if value + ) + return tuple(endpoints) + + def bind_wire_task_terminals_from_payload(doc, payload): """Bind local template terminals to QET terminal UUIDs without creating wires.""" if doc is None: @@ -1175,8 +1187,9 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la if isinstance(prepared_layout, dict): report["prepared_layout"] = prepared_layout missing_endpoint_uuids = set() + lane_indexes_by_pair = {} - for index, item in enumerate(wires): + for item in wires: if not isinstance(item, dict): report["skipped_invalid"] += 1 continue @@ -1206,12 +1219,14 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } ) continue + lane_key = _route_lane_key(start_uuid, end_uuid) + route_lane_index = lane_indexes_by_pair.get(lane_key, 0) try: result = route_eplan_connection_between_terminals( doc, start_terminal, end_terminal, - route_index=index, + route_index=route_lane_index, options=options, wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), wire_label=_wire_item_value(item, "wire_label", "wire_mark"), @@ -1224,6 +1239,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la except Exception as exc: report["errors"].append(str(exc)) continue + lane_indexes_by_pair[lane_key] = route_lane_index + 1 if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 route_collision_samples = [] diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index f57050c..7648c1f 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1241,6 +1241,59 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, route["network"]["carriers"]) self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + def test_route_eplan_connections_lane_index_is_per_terminal_pair(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _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, 100, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 100, 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, 100, 20), app.Vector(100, 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", + }, + { + "wire_id": "wire-a-repeat", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + + self.assertEqual(0, report["routes"][0]["lane"]["index"]) + self.assertEqual(0, report["routes"][1]["lane"]["index"]) + self.assertEqual(1, report["routes"][2]["lane"]["index"]) + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 5c813b66dd6df751dfacffe05fea277ab509cd86 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 15:57:42 +0800 Subject: [PATCH 05/63] =?UTF-8?q?fix:=20=E5=A2=9E=E5=BC=BAFreeCAD=E4=B8=89?= =?UTF-8?q?=E7=BB=B4=E5=AF=BC=E5=85=A5=E5=92=8C=E5=B8=83=E7=BA=BF=E8=AF=8A?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 14 ++- src/Mod/FreeCADExchange/DeviceImport.py | 54 ++++++--- .../freecad_exchange_auto_routing_test.py | 35 ++++++ ...eecad_exchange_device_import_fcstd_test.py | 105 ++++++++++++++++++ 4 files changed, 193 insertions(+), 15 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index e259a9a..544385b 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1182,6 +1182,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "missing_endpoint_samples": [], "collision_samples": [], "errors": [], + "error_samples": [], "routes": [], } if isinstance(prepared_layout, dict): @@ -1237,7 +1238,18 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la wire_style_id=_wire_item_value(item, "wire_style_id"), ) except Exception as exc: - report["errors"].append(str(exc)) + error_text = str(exc) + report["errors"].append(error_text) + if len(report["error_samples"]) < 8: + report["error_samples"].append( + { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "start_terminal_uuid": start_uuid, + "end_terminal_uuid": end_uuid, + "error": error_text, + } + ) continue lane_indexes_by_pair[lane_key] = route_lane_index + 1 if result["route_status"] == "CollisionWarning": diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index 646c3e3..f9cbb4f 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -629,16 +629,6 @@ def _remove_template_terminal_hints(doc, container): return removed -def _clear_group_contents(doc, group): - for child in list(getattr(group, "Group", []) or []): - child_name = getattr(child, "Name", "") - if child_name.startswith(TERMINAL_GROUP_PREFIX) or child_name.startswith(WIRE_GROUP_PREFIX): - continue - if getattr(child, "QetGroupKind", "").strip() in {GROUP_KIND_TERMINALS, GROUP_KIND_WIRES}: - continue - _remove_object_tree(doc, child) - - def _existing_group_objects(doc, group): result = [] for child in list(getattr(group, "Group", []) or []): @@ -654,6 +644,19 @@ def _is_exchange_sidecar_group(obj): return getattr(obj, "QetGroupKind", "").strip() in {GROUP_KIND_TERMINALS, GROUP_KIND_WIRES} +def _existing_model_objects(doc, group): + return [ + obj + for obj in _existing_group_objects(doc, group) + if not _is_exchange_sidecar_group(obj) + ] + + +def _remove_model_objects(doc, objects): + for obj in list(objects or []): + _remove_object_tree(doc, obj) + + def _keep_only_direct_model_children(device_group, direct_model_objects): allowed_ids = {id(obj) for obj in direct_model_objects if obj is not None} kept_children = [] @@ -902,9 +905,12 @@ def _import_cabinet_model(doc, root_group, cabinet, report): return project_uuid = getattr(root_group, "QetProjectUuid", "").strip() + existing_group = _find_cabinet_group(doc, _cabinet_instance_id(cabinet)) + previous_path = "" + if existing_group is not None: + previous_path = getattr(existing_group, "QetCabinetResolvedScenePath", "").strip() cabinet_group = _ensure_cabinet_model_group(doc, root_group, cabinet, project_uuid) - existing_model_objects = _existing_group_objects(doc, cabinet_group) - previous_path = getattr(cabinet_group, "QetCabinetResolvedScenePath", "").strip() + existing_model_objects = _existing_model_objects(doc, cabinet_group) same_source = _normalized_path_key(previous_path) == _normalized_path_key(resolved_scene_path) if existing_model_objects and same_source: report.setdefault("cabinet_reused", 0) @@ -917,7 +923,6 @@ def _import_cabinet_model(doc, root_group, cabinet, report): return had_existing_model = bool(existing_model_objects) - _clear_group_contents(doc, cabinet_group) _ensure_string_property( cabinet_group, "QetCabinetResolvedScenePath", @@ -938,6 +943,7 @@ def _import_cabinet_model(doc, root_group, cabinet, report): merge=False, use_link_group=True, ) + _remove_model_objects(doc, existing_model_objects) report["cabinet_imported"] += 1 if had_existing_model: report.setdefault("cabinet_reimported", 0) @@ -947,6 +953,14 @@ def _import_cabinet_model(doc, root_group, cabinet, report): report["cabinet_added"] += 1 _append_debug_log("DeviceImport cabinet import succeeded") except Exception as exc: + if had_existing_model: + _ensure_string_property( + cabinet_group, + "QetCabinetResolvedScenePath", + "QET Exchange", + "Resolved local cabinet scene path from QET exchange", + previous_path, + ) report["cabinet_skipped_import_error"] += 1 report["warnings"].append( "机柜 3D 导入失败:{0}".format(exc) @@ -1039,6 +1053,9 @@ def import_devices_from_payload(payload, scene_path=""): instance_id = existing_instance_id or _generate_instance_id(project_uuid, element_uuid) report.setdefault("generated_instance_ids", 0) report["generated_instance_ids"] += 1 + previous_model_path = "" + if existing_group is not None: + previous_model_path = getattr(existing_group, "QetResolvedModelPath", "").strip() device_group, created_now = _ensure_device_group( doc, root_group, @@ -1048,7 +1065,7 @@ def import_devices_from_payload(payload, scene_path=""): display_tag, index, ) - _clear_group_contents(doc, device_group) + existing_model_objects = _existing_model_objects(doc, device_group) try: _append_debug_log( @@ -1060,7 +1077,16 @@ def import_devices_from_payload(payload, scene_path=""): _append_debug_log( "DeviceImport import succeeded for element_uuid={0}".format(element_uuid) ) + _remove_model_objects(doc, existing_model_objects) except Exception as exc: + if existing_model_objects: + _ensure_string_property( + device_group, + "QetResolvedModelPath", + "QET Exchange", + "Resolved local model path from QET exchange", + previous_model_path, + ) report["skipped_import_error"] += 1 report["warnings"].append( "{0} 导入失败:{1}".format( diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 7648c1f..8d1ffbc 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1241,6 +1241,41 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, route["network"]["carriers"]) self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + def test_route_eplan_connections_records_wire_identity_for_errors(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, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-bad", + "wire_label": "N500", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-start", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(1, len(report["errors"])) + self.assertIn("error_samples", report) + self.assertEqual("wire-bad", report["error_samples"][0]["wire_uuid"]) + self.assertEqual("N500", report["error_samples"][0]["wire_label"]) + self.assertEqual("terminal-start", report["error_samples"][0]["start_terminal_uuid"]) + self.assertEqual("terminal-start", report["error_samples"][0]["end_terminal_uuid"]) + self.assertIn("different", report["error_samples"][0]["error"]) + def test_route_eplan_connections_lane_index_is_per_terminal_pair(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() diff --git a/tests/python/freecad_exchange_device_import_fcstd_test.py b/tests/python/freecad_exchange_device_import_fcstd_test.py index ec567d5..0326859 100644 --- a/tests/python/freecad_exchange_device_import_fcstd_test.py +++ b/tests/python/freecad_exchange_device_import_fcstd_test.py @@ -200,6 +200,9 @@ class FakeDocument: if target in getattr(obj, "InList", []): obj.InList.remove(target) + def recompute(self): + return None + def copyObject(self, source_obj, recursive): copies = {} @@ -355,6 +358,108 @@ class FcstdDeviceImportTest(unittest.TestCase): self.assertIn(group_a, root_group.Group) self.assertIn(group_b, root_group.Group) + def test_failed_cabinet_reimport_keeps_existing_model(self): + with tempfile.TemporaryDirectory() as temp_dir: + cabinet_path = Path(temp_dir) / "cabinet.step" + cabinet_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root_group = device_import._ensure_root_group(doc, None, "project-1") + cabinet = { + "location_id": 1, + "resolved_scene_path": str(cabinet_path), + "display_text": "Main Cabinet", + } + cabinet_group = device_import._ensure_cabinet_model_group( + doc, + root_group, + cabinet, + "project-1", + ) + old_cabinet_path = str(Path(temp_dir) / "old-cabinet.step") + cabinet_group.QetCabinetResolvedScenePath = old_cabinet_path + old_body = doc.addObject("Part::Feature", "OldCabinetBody") + cabinet_group.addObject(old_body) + + def failing_import(*_args, **_kwargs): + raise RuntimeError("simulated import failure") + + device_import._import_model_into_group = failing_import + report = { + "cabinet_imported": 0, + "cabinet_added": 0, + "cabinet_reimported": 0, + "cabinet_reused": 0, + "cabinet_skipped_missing_model": 0, + "cabinet_skipped_missing_file": 0, + "cabinet_skipped_unsupported_format": 0, + "cabinet_skipped_import_error": 0, + "warnings": [], + } + device_import._import_cabinet_model(doc, root_group, cabinet, report) + + self.assertEqual(1, report["cabinet_skipped_import_error"]) + self.assertIs(doc.getObject("OldCabinetBody"), old_body) + self.assertIn(old_body, cabinet_group.Group) + self.assertEqual(old_cabinet_path, cabinet_group.QetCabinetResolvedScenePath) + + def test_failed_device_reimport_keeps_existing_model(self): + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "breaker.step" + model_path.write_text("fake step placeholder", encoding="utf-8") + _install_fake_freecad(None) + + device_import, _ = _reload_modules() + + doc = FakeDocument("QETScene") + root_group = device_import._ensure_root_group(doc, None, "project-1") + device_group, _created = device_import._ensure_device_group( + doc, + root_group, + "device-1", + "instance-1", + str(model_path), + "QF1", + 0, + ) + old_model_path = str(Path(temp_dir) / "old-breaker.step") + device_group.QetResolvedModelPath = old_model_path + old_body = doc.addObject("Part::Feature", "OldDeviceBody") + device_group.addObject(old_body) + + def failing_import(*_args, **_kwargs): + raise RuntimeError("simulated import failure") + + device_import._import_model_into_group = failing_import + sys.modules["DevicePreview"].find_main_exchange_document = lambda _name: doc + + report = device_import.import_devices_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "element_uuid": "device-1", + "instance_id": "instance-1", + "display_tag": "QF1", + } + ], + "device_models": [ + { + "element_uuid": "device-1", + "resolved_model_path": str(model_path), + } + ], + } + ) + + self.assertEqual(1, report["skipped_import_error"]) + self.assertIs(doc.getObject("OldDeviceBody"), old_body) + self.assertIn(old_body, device_group.Group) + self.assertEqual(old_model_path, device_group.QetResolvedModelPath) + def test_fcstd_import_preserves_template_slots_without_live_template_lcs(self): source = FakeDocument("Source", r"D:\models\breaker.FCStd") _install_fake_freecad(source) From a2577f943487687c0b5bd980521fbf8eaeca2376 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:03:32 +0800 Subject: [PATCH 06/63] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96FreeCAD=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E5=B9=B6=E7=BA=BF=E5=92=8C=E7=A2=B0?= =?UTF-8?q?=E6=92=9E=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 16 ++++++++++++++-- .../python/freecad_exchange_auto_routing_test.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 544385b..502c855 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -144,13 +144,19 @@ def _lane_payload(route_index, options): lane_axis = (opts.get("lane_axis") or "y").lower() if lane_axis not in {"x", "y", "z"}: lane_axis = "y" - lane_index = int(route_index or 0) + lane_index = max(int(route_index or 0), 0) lane_spacing = float(opts.get("lane_spacing", 0.0) or 0.0) + if lane_index <= 0: + lane_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 return { "index": lane_index, "axis": lane_axis, "spacing_mm": lane_spacing, - "offset_mm": float(lane_index) * lane_spacing, + "offset_mm": lane_offset, } @@ -809,6 +815,7 @@ def collect_obstacles(doc, exclude=None, options=None): continue if RoutingNetwork.is_route_carrier(obj) or WiringObjects.is_routed_wire_object(obj): continue + raw_bbox = _bbox_payload(obj, clearance=0.0) bbox = _bbox_payload(obj, clearance=clearance) if bbox is None: continue @@ -823,6 +830,7 @@ def collect_obstacles(doc, exclude=None, options=None): "label": getattr(obj, "Label", ""), "type_id": getattr(obj, "TypeId", ""), "bbox": bbox, + "raw_bbox": raw_bbox or bbox, } ) return obstacles @@ -871,8 +879,12 @@ def detect_collisions(points, obstacles, ignored_segment_indices=None): collisions.append( { "segment_index": index, + "segment_start": _point_payload(start), + "segment_end": _point_payload(end), "obstacle_name": obstacle.get("name", ""), "obstacle_label": obstacle.get("label", ""), + "obstacle_bbox": dict(obstacle.get("raw_bbox") or obstacle.get("bbox") or {}), + "collision_bbox": dict(obstacle.get("bbox", {}) or {}), } ) return collisions diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 8d1ffbc..4589cd7 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -312,13 +312,25 @@ class AutoRoutingTest(unittest.TestCase): wire_uuid="wire-2", options={"lane_spacing": 12.0, "lane_axis": "y"}, ) + third = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + route_index=2, + wire_uuid="wire-3", + options={"lane_spacing": 12.0, "lane_axis": "y"}, + ) payload = json.loads(second["wire"].QetRouteDiagnosticsJson) + third_payload = json.loads(third["wire"].QetRouteDiagnosticsJson) self.assertTrue(any(abs(point.y - 0.0) <= 0.001 for point in first["points"][1:-1])) self.assertTrue(any(abs(point.y - 12.0) <= 0.001 for point in second["points"][1:-1])) + self.assertTrue(any(abs(point.y + 12.0) <= 0.001 for point in third["points"][1:-1])) self.assertEqual(1, payload["lane"]["index"]) self.assertEqual("y", payload["lane"]["axis"]) self.assertEqual(12.0, payload["lane"]["offset_mm"]) + self.assertEqual(2, third_payload["lane"]["index"]) + self.assertEqual(-12.0, third_payload["lane"]["offset_mm"]) def test_eplan_connection_route_replaces_existing_wire_uuid_when_endpoints_change(self): _install_fake_freecad() @@ -1364,6 +1376,10 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) self.assertEqual("N4111", report["collision_samples"][0]["wire_label"]) self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_start"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_end"]) + self.assertEqual(40.0, report["collision_samples"][0]["obstacle_bbox"]["xmin"]) + self.assertEqual(35.0, report["collision_samples"][0]["collision_bbox"]["xmin"]) self.assertEqual("Middle Obstacle", report["routes"][0]["collision_samples"][0]["obstacle_label"]) self.assertIn("碰撞示例", message) self.assertIn("Middle Obstacle", message) From 0b0c32e0c3c6fe4f2b79f4872d0db4a357d4b78f Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:09:01 +0800 Subject: [PATCH 07/63] =?UTF-8?q?feat:=20=E4=BF=9D=E7=95=99FreeCAD?= =?UTF-8?q?=E5=B7=B2=E5=B8=83=E7=BA=BF=E8=AF=8A=E6=96=AD=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/WiringObjects.py | 17 ++++++++++ tests/python/freecad_exchange_wiring_test.py | 35 ++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Mod/FreeCADExchange/WiringObjects.py b/src/Mod/FreeCADExchange/WiringObjects.py index 719e83f..43d2360 100644 --- a/src/Mod/FreeCADExchange/WiringObjects.py +++ b/src/Mod/FreeCADExchange/WiringObjects.py @@ -314,6 +314,21 @@ def _json_property(obj, prop_name, fallback=None): return fallback +def _float_property(obj, prop_name, fallback=0.0): + try: + return float(getattr(obj, prop_name, fallback) or fallback) + except Exception: + return float(fallback) + + +def _route_metadata_payload(wire_obj): + return { + "wire_style_id": getattr(wire_obj, "QetWireStyleId", "").strip(), + "length_mm": _float_property(wire_obj, "QetRouteLengthMm", 0.0), + "route_diagnostics": _json_property(wire_obj, "QetRouteDiagnosticsJson", {}), + } + + def wire_shape_points(wire_obj): if wire_obj is None: return [] @@ -374,6 +389,7 @@ def wire_payload_from_object(wire_obj): "route_nodes": [], "route_track": {}, "terminal_exit_length": float(getattr(wire_obj, "QetTerminalExitLength", 0.0) or 0.0), + **_route_metadata_payload(wire_obj), } points = [_point_from_vector(point) for point in wire_shape_points(wire_obj)] return { @@ -395,6 +411,7 @@ def wire_payload_from_object(wire_obj): "route_nodes": _json_array_property(wire_obj, "QetRouteNodesJson"), "route_track": _json_property(wire_obj, "QetRouteTrackJson", {}), "terminal_exit_length": float(getattr(wire_obj, "QetTerminalExitLength", 0.0) or 0.0), + **_route_metadata_payload(wire_obj), } diff --git a/tests/python/freecad_exchange_wiring_test.py b/tests/python/freecad_exchange_wiring_test.py index 4a1a313..29bcb3f 100644 --- a/tests/python/freecad_exchange_wiring_test.py +++ b/tests/python/freecad_exchange_wiring_test.py @@ -1,3 +1,4 @@ +import json import os import sys import tempfile @@ -341,6 +342,40 @@ class WiringTest(unittest.TestCase): ) self.assertEqual("face", payload["route_nodes"][2]["anchor_kind"]) + def test_wire_payload_includes_auto_route_diagnostics_metadata(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _manual_wiring, _write_back = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + wire = doc.addObject("Part::Feature", "QETWire_auto") + wire.Shape = [app.Vector(0, 0, 0), app.Vector(100, 0, 0)] + terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1") + terminal_objects.ensure_string_property(wire, "QetWireUuid", "QET Exchange", "", "wire-1") + terminal_objects.ensure_string_property(wire, "QetWireLabel", "QET Exchange", "", "N4111") + terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "terminal-start") + terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-end") + terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-start") + terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-end") + terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "RoutedConnection") + terminal_objects.ensure_string_property(wire, "RouteStatus", "QET Exchange", "", "CollisionWarning") + terminal_objects.ensure_string_property(wire, "RouteMode", "QET Exchange", "", "EplanRoute") + terminal_objects.ensure_string_property(wire, "QetWireStyleId", "QET Exchange", "", "42") + terminal_objects.ensure_string_property(wire, "QetRouteLengthMm", "QET Exchange", "", "123.5") + terminal_objects.ensure_string_property( + wire, + "QetRouteDiagnosticsJson", + "QET Exchange", + "", + json.dumps({"collision_count": 1, "wire_style_id": "42"}), + ) + + payload = wiring_objects.wire_payload_from_object(wire) + + self.assertEqual("42", payload["wire_style_id"]) + self.assertEqual(123.5, payload["length_mm"]) + self.assertEqual(1, payload["route_diagnostics"]["collision_count"]) + def test_wire_writeback_omits_scene_routed_wire_payload(self): _install_fake_freecad() terminal_objects, wiring_objects, manual_wiring, write_back = _reload_modules() From aaa2fd365441d90fd54955679f07ec82dbc15940 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:21:15 +0800 Subject: [PATCH 08/63] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BAFreeCAD?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=BB=93=E6=9E=9C=E6=B1=87?= =?UTF-8?q?=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 35 +++++++++ .../freecad_exchange_auto_routing_test.py | 74 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 502c855..3ff04f4 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1195,6 +1195,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "collision_samples": [], "errors": [], "error_samples": [], + "route_status_counts": {}, "routes": [], } if isinstance(prepared_layout, dict): @@ -1202,9 +1203,14 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la missing_endpoint_uuids = set() lane_indexes_by_pair = {} + def add_status(status): + key = str(status or "").strip() or "Unknown" + report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 + for item in wires: if not isinstance(item, dict): report["skipped_invalid"] += 1 + add_status("Invalid") continue start_uuid = _wire_item_value(item, "start_terminal_uuid") end_uuid = _wire_item_value(item, "end_terminal_uuid") @@ -1212,6 +1218,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la end_terminal = terminals.get(end_uuid) if start_terminal is None or end_terminal is None: report["skipped_missing_terminal"] += 1 + add_status("MissingTerminal") for terminal_uuid in (start_uuid, end_uuid): if terminal_uuid and terminal_uuid not in terminals: missing_endpoint_uuids.add(terminal_uuid) @@ -1252,6 +1259,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la except Exception as exc: error_text = str(exc) report["errors"].append(error_text) + add_status("Error") if len(report["error_samples"]) < 8: report["error_samples"].append( { @@ -1266,6 +1274,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la lane_indexes_by_pair[lane_key] = route_lane_index + 1 if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 + add_status(result["route_status"]) route_collision_samples = [] for collision in list(result.get("collisions", []) or [])[:3]: sample = dict(collision) @@ -1311,6 +1320,32 @@ def format_eplan_connection_route_report(report): report.get("collision_warnings", 0), report.get("skipped_missing_terminal", 0), ) + status_counts = report.get("route_status_counts", {}) + if isinstance(status_counts, dict) and status_counts: + status_labels = { + "Routed": "正常", + "CollisionWarning": "碰撞告警", + "Error": "错误", + "MissingTerminal": "缺失端子", + "Invalid": "无效任务", + } + def status_count_value(value): + try: + return int(value or 0) + except Exception: + return 0 + status_parts = [] + for key in ("Routed", "CollisionWarning", "Error", "MissingTerminal", "Invalid"): + value = status_count_value(status_counts.get(key, 0)) + if value > 0: + status_parts.append("{0} {1} 条".format(status_labels[key], value)) + for key, value in sorted(status_counts.items()): + value = status_count_value(value) + if key in status_labels or value <= 0: + continue + status_parts.append("{0} {1} 条".format(key, value)) + if status_parts: + message += "\n结果状态:{0}。".format(",".join(status_parts)) prepared_layout = report.get("prepared_layout") if isinstance(prepared_layout, dict): message += "\n布线布局空间:线槽路径 {0} 条,布线面 {1} 条,端子接入 {2} 条。".format( diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 4589cd7..29ef297 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1288,6 +1288,62 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("terminal-start", report["error_samples"][0]["end_terminal_uuid"]) self.assertIn("different", report["error_samples"][0]["error"]) + def test_route_eplan_connections_counts_route_statuses_for_summary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "RouteStart", "route-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "RouteEnd", "route-end", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "CollisionStart", "collision-start", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "CollisionEnd", "collision-end", app.Vector(100, 100, 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, 100, 100), app.Vector(100, 100, 100)], + project_uuid="project-1", + kind="WireDuct", + ) + obstacle = doc.addObject("Part::Feature", "CollisionObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 90, 110, 90, 110)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-ok", + "start_terminal_uuid": "route-start", + "end_terminal_uuid": "route-end", + }, + { + "wire_id": "wire-collision", + "start_terminal_uuid": "collision-start", + "end_terminal_uuid": "collision-end", + }, + { + "wire_id": "wire-error", + "start_terminal_uuid": "route-start", + "end_terminal_uuid": "route-start", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual(1, report["route_status_counts"]["Routed"]) + self.assertEqual(1, report["route_status_counts"]["CollisionWarning"]) + self.assertEqual(1, report["route_status_counts"]["Error"]) + self.assertIn("结果状态", message) + self.assertIn("正常 1 条", message) + self.assertIn("碰撞告警 1 条", message) + self.assertIn("错误 1 条", message) + def test_route_eplan_connections_lane_index_is_per_terminal_pair(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -1445,6 +1501,24 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) self.assertIn("缺失示例:terminal-a -> terminal-b", message) + def test_route_eplan_connections_report_ignores_non_numeric_status_counts(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "route_status_counts": { + "Routed": "1", + "ExternalStatus": "not-a-number", + }, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("正常 1 条", message) + self.assertNotIn("ExternalStatus", message) + def test_bind_wire_task_terminals_from_payload_does_not_create_wires(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() From 0e42c7db0ab55b3797201da02cab0454750e18f9 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:27:59 +0800 Subject: [PATCH 09/63] =?UTF-8?q?feat:=20=E5=8C=BA=E5=88=86FreeCAD?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=A2=B0=E6=92=9E=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 22 ++++++-- .../freecad_exchange_auto_routing_test.py | 52 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 3ff04f4..9d23d76 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -876,14 +876,19 @@ def detect_collisions(points, obstacles, ignored_segment_indices=None): end = points[index + 1] for obstacle in obstacles: if _segment_intersects_bbox(start, end, obstacle["bbox"]): + raw_bbox = obstacle.get("raw_bbox") or obstacle.get("bbox") or {} + collision_kind = "HardIntersection" + if raw_bbox and not _segment_intersects_bbox(start, end, raw_bbox): + collision_kind = "ClearanceWarning" collisions.append( { "segment_index": index, "segment_start": _point_payload(start), "segment_end": _point_payload(end), + "collision_kind": collision_kind, "obstacle_name": obstacle.get("name", ""), "obstacle_label": obstacle.get("label", ""), - "obstacle_bbox": dict(obstacle.get("raw_bbox") or obstacle.get("bbox") or {}), + "obstacle_bbox": dict(raw_bbox), "collision_bbox": dict(obstacle.get("bbox", {}) or {}), } ) @@ -1366,12 +1371,21 @@ def format_eplan_connection_route_report(report): or collision_sample.get("obstacle_name") or "未知对象" ) - message += "\n碰撞示例:导线 {0} 碰到 {1}。".format( + wire_text = ( collision_sample.get("wire_label") or collision_sample.get("wire_uuid") - or "未知导线", - obstacle_text, + or "未知导线" ) + if collision_sample.get("collision_kind") == "ClearanceWarning": + message += "\n碰撞示例:导线 {0} 进入 {1} 的安全间隙。".format( + wire_text, + obstacle_text, + ) + else: + message += "\n碰撞示例:导线 {0} 碰到 {1}。".format( + wire_text, + obstacle_text, + ) auto_bound = report.get("auto_bound_terminals", 0) auto_created = report.get("auto_created_terminals", 0) if auto_bound or auto_created: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 29ef297..82baa0a 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1074,6 +1074,36 @@ class AutoRoutingTest(unittest.TestCase): 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"]) + + 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") + 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", "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}, + ) + + 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"]) def test_eplan_connection_route_ignores_terminal_exit_segment_collision(self): _install_fake_freecad() @@ -1432,6 +1462,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) self.assertEqual("N4111", report["collision_samples"][0]["wire_label"]) self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) + self.assertEqual("HardIntersection", report["collision_samples"][0]["collision_kind"]) self.assertEqual({"x": 0.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_start"]) self.assertEqual({"x": 100.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_end"]) self.assertEqual(40.0, report["collision_samples"][0]["obstacle_bbox"]["xmin"]) @@ -1501,6 +1532,27 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) self.assertIn("缺失示例:terminal-a -> terminal-b", message) + def test_route_eplan_connections_report_calls_out_clearance_collision_kind(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 1, + "skipped_missing_terminal": 0, + "collision_samples": [ + { + "wire_label": "N4111", + "obstacle_label": "柜体侧板", + "collision_kind": "ClearanceWarning", + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("安全间隙", message) + self.assertIn("柜体侧板", message) + def test_route_eplan_connections_report_ignores_non_numeric_status_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() From f74e55b7821a330849a29b99fbe1eac58d81576b Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:34:48 +0800 Subject: [PATCH 10/63] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BAFreeCAD?= =?UTF-8?q?=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84=E7=BD=91=E7=BB=9C=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 70 +++++++++++++++++++ src/Mod/FreeCADExchange/AutoRoutingPanel.py | 21 +----- .../freecad_exchange_auto_routing_test.py | 20 ++++++ 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 9d23d76..bbc1eed 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1580,6 +1580,76 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None): } +def _format_distance_mm(value): + try: + return "{0:.1f} mm".format(float(value)) + except Exception: + return "未知距离" + + +def _diagnostic_terminal_text(item): + if not isinstance(item, dict): + return "未知端子" + return ( + item.get("terminal_uuid") + or item.get("label") + or item.get("name") + or "未知端子" + ) + + +def _dict_items(value): + if not isinstance(value, list): + return [] + return [item for item in value if isinstance(item, dict)] + + +def format_routing_path_network_report(diagnostic): + """Return an actionable Chinese summary for routing path network diagnostics.""" + if not isinstance(diagnostic, dict): + return "布线路径网络检查失败:诊断结果无效。" + + summary = diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {} + issues = _dict_items(diagnostic.get("issues", []) or []) + if not issues: + return "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。".format( + summary.get("carriers", 0), + summary.get("segments", 0), + summary.get("nodes", 0), + ) + + message = "布线路径网络检查发现 {0} 类问题。".format(len(issues)) + unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or []) + if unconnected: + sample = unconnected[0] + message += "\n端子未接入:{0},距离最近网络 {1}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format( + _diagnostic_terminal_text(sample), + _format_distance_mm(sample.get("nearest_network_distance_mm")), + ) + + possible_breaks = _dict_items(diagnostic.get("possible_breaks", []) or []) + if possible_breaks: + sample = possible_breaks[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + carrier_text = carrier.get("label") or carrier.get("name") or "未知线槽" + message += "\n线槽端点疑似断开:{0}。请补齐相邻线槽、开口或辅助路径。".format(carrier_text) + + isolated = _dict_items(diagnostic.get("isolated_components", []) or []) + if isolated: + sample = isolated[0] + carriers = sample.get("carrier_labels") or sample.get("carrier_names") or [] + carrier_text = "、".join([str(item) for item in carriers[:3]]) if carriers else "未知 carrier" + message += "\n存在孤立路径网络:{0}。请用线槽/辅助路径把孤立网络接入主网络。".format(carrier_text) + + if not (unconnected or possible_breaks or isolated): + first_issue = issues[0] + message += "\n首个问题:{0} ({1})。".format( + first_issue.get("code", "unknown"), + first_issue.get("count", 0), + ) + return message + + def update_eplan_routing_path_network(doc, project_uuid="", options=None, selection_ex=None): """Update the routing path network before EPLAN-style Route.""" return generate_eplan_routing_path_network( diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index fb969cc..e5e3906 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -276,25 +276,10 @@ class AutoRoutingTaskPanel: try: result = self.controller.check_routing_path_network() diagnostic = result.get("diagnostic", {}) if isinstance(result.get("diagnostic", {}), dict) else {} - issues = diagnostic.get("issues", []) or [] - summary = diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {} - if not issues: - self._set_status( - "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。{3}".format( - summary.get("carriers", 0), - summary.get("segments", 0), - summary.get("nodes", 0), - self.controller.summary(), - ) - ) - return - first_issue = issues[0] self._set_status( - "布线路径网络检查发现 {0} 类问题:{1} ({2})。{3}".format( - len(issues), - first_issue.get("code", ""), - first_issue.get("count", 0), - self.controller.summary(), + "{0}{1}".format( + AutoRouting.format_routing_path_network_report(diagnostic), + "\n" + self.controller.summary(), ) ) except Exception as exc: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 82baa0a..c4ae723 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -765,6 +765,26 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) self.assertEqual(1, len(payload["unconnected_terminals"])) self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"]) + 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("补一段线槽/辅助路径", message) + + 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) def test_generate_routing_path_network_skips_far_terminal_access_to_protect_view_bbox(self): _install_fake_freecad() From f903badd22f478da285ac04cc3b22b34e00cfd8a Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:44:28 +0800 Subject: [PATCH 11/63] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96FreeCAD?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E5=85=A5=E7=BD=91=E7=82=B9?= =?UTF-8?q?=E6=8A=95=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 6 +- src/Mod/FreeCADExchange/RoutingNetwork.py | 85 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 68 ++++++++++++++- 3 files changed, 156 insertions(+), 3 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index bbc1eed..5ec44c7 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -629,8 +629,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non if network.get("segment_count", 0) <= 0: return None - start_key, start_distance = RoutingNetwork.nearest_node(network, start_exit) - end_key, end_distance = RoutingNetwork.nearest_node(network, end_exit) + start_key, start_distance, start_mode = RoutingNetwork.connect_point_to_network(network, start_exit) + end_key, end_distance, end_mode = RoutingNetwork.connect_point_to_network(network, end_exit) if start_key is None or end_key is None: return None @@ -677,6 +677,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "nodes": len(network.get("nodes", {})), "entry_distance": float(start_distance or 0.0), "exit_distance": float(end_distance or 0.0), + "entry_point_mode": start_mode, + "exit_point_mode": end_mode, "obstacle_aware": bool(obstacle_aware), }, "route_track": path_result, diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 43862d7..fb38574 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2177,6 +2177,91 @@ def nearest_point_on_network(network, point): return nearest_node(network, target) +def connect_point_to_network(network, point): + """Connect the closest projected point to a route graph and return key/distance/mode.""" + if not isinstance(network, dict): + return None, None, "none" + nodes = network.get("nodes", {}) or {} + edges = network.get("edges", {}) or {} + if not nodes or not edges: + return None, None, "none" + + tolerance = float(network.get("tolerance", DEFAULT_NODE_TOLERANCE) or DEFAULT_NODE_TOLERANCE) + target = _vector(point) + best = None + seen = set() + for key, neighbors in edges.items(): + start = nodes.get(key) + if start is None: + continue + for next_key, _weight, carrier in neighbors: + pair = tuple(sorted((key, next_key))) + if pair in seen: + continue + seen.add(pair) + end = nodes.get(next_key) + if end is None: + continue + projected = _closest_point_on_segment(target, start, end) + distance = _distance(target, projected) + if best is None or distance < best["distance"]: + best = { + "key": key, + "next_key": next_key, + "carrier": carrier, + "point": projected, + "distance": distance, + } + + if best is None: + node_key, distance = nearest_node(network, target) + return node_key, distance, "node" if node_key is not None else "none" + + projected_key = _point_key(best["point"], tolerance=tolerance) + if projected_key in nodes: + return projected_key, best["distance"], "node" + + start_key = best["key"] + end_key = best["next_key"] + start = nodes[start_key] + end = nodes[end_key] + carrier = best["carrier"] + + def remove_edge_once(left_key, right_key, fallback_to_pair=False): + neighbors = list(edges.get(left_key, []) or []) + for index, (candidate_key, _weight, candidate_carrier) in enumerate(neighbors): + if candidate_key == right_key and candidate_carrier is carrier: + del neighbors[index] + edges[left_key] = neighbors + return True + if fallback_to_pair: + for index, (candidate_key, _weight, _candidate_carrier) in enumerate(neighbors): + if candidate_key == right_key: + del neighbors[index] + edges[left_key] = neighbors + return True + return False + + removed_forward = remove_edge_once(start_key, end_key) + remove_edge_once(end_key, start_key, fallback_to_pair=removed_forward) + + nodes[projected_key] = best["point"] + edges[projected_key] = [] + added_segments = 0 + for left_key, left_point, right_key, right_point in ( + (start_key, start, projected_key, best["point"]), + (projected_key, best["point"], end_key, end), + ): + weight = _distance(left_point, right_point) + if weight <= tolerance: + continue + edges[left_key].append((right_key, weight, carrier)) + edges[right_key].append((left_key, weight, carrier)) + added_segments += 1 + network["segment_count"] = max(int(network.get("segment_count", 0) or 0) - 1 + added_segments, 0) + return projected_key, best["distance"], "segment_projection" + + def _carrier_track_payload(carrier): return { "name": getattr(carrier, "Name", ""), diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index c4ae723..5a43b75 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -480,6 +480,45 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) + def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + network = routing_network.build_route_graph(doc) + original_keys = set(network["nodes"].keys()) + bridge_keys = { + key + for key, point in network["nodes"].items() + if point.x in {50.0, 54.0} + } + + projected_key, _distance, mode = routing_network.connect_point_to_network(network, app.Vector(52, 0, 20)) + new_keys = set(network["nodes"].keys()) - original_keys + stale_bridge_edges = [ + (left_key, right_key) + for left_key, neighbors in network["edges"].items() + for right_key, _weight, _carrier in neighbors + if left_key in bridge_keys and right_key in bridge_keys + ] + + self.assertEqual("segment_projection", mode) + self.assertEqual(projected_key, next(iter(new_keys))) + self.assertEqual([], stale_bridge_edges) + self.assertEqual(4, network["segment_count"]) + def test_eplan_connection_route_prefers_wire_duct_over_auxiliary_range(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -719,6 +758,29 @@ class AutoRoutingTest(unittest.TestCase): 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_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(200, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + 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) + 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() @@ -1383,7 +1445,11 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"avoid_obstacles": False}, + ) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["route_status_counts"]["Routed"]) From 065ae182e36a3b41c496cdcc83dd9a08071691a8 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:50:06 +0800 Subject: [PATCH 12/63] =?UTF-8?q?fix:=20=E4=BF=9D=E7=95=99FreeCAD=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E5=A4=B1=E8=B4=A5=E5=89=8D=E7=9A=84?= =?UTF-8?q?=E6=97=A7=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 6 ++-- .../freecad_exchange_auto_routing_test.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 5ec44c7..bba9eb4 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1028,9 +1028,6 @@ def route_eplan_connection_between_terminals( if not project_uuid: raise AutoRoutingError("Project UUID is required for routing connections.") - if opts.get("replace_existing", True): - _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=wire_uuid) - route_data = build_network_route( start_terminal, end_terminal, @@ -1054,6 +1051,9 @@ def route_eplan_connection_between_terminals( collisions = detect_collisions(points, obstacles, ignored_segment_indices=ignored_collision_segments) status = "CollisionWarning" if collisions else "Routed" + if opts.get("replace_existing", True): + _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=wire_uuid) + wire_name = _unique_name(doc, _wire_object_name(start_terminal, end_terminal, wire_uuid)) wire = _create_wire_geometry(doc, wire_name, points) wire.Label = wire_label or wire_mark or wire_uuid or "QET Routed Connection" diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 5a43b75..cb39904 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -362,6 +362,40 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("terminal-start-new", routed_wires[0].QetStartTerminalUuid) self.assertEqual("terminal-end-new", routed_wires[0].QetEndTerminalUuid) + def test_eplan_connection_route_keeps_existing_wire_when_replacement_fails(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + first = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + )["wire"] + routing_network.clear_route_carriers(doc) + + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual([first], routed_wires) + self.assertIsNotNone(doc.getObject(first.Name)) + def test_route_carrier_styles_make_generated_objects_distinguishable(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() From 41eec935d50e7ab18bd3d46d6658f1de61fd2edc Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:01:13 +0800 Subject: [PATCH 13/63] =?UTF-8?q?fix:=20=E8=87=AA=E5=8A=A8=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E5=A4=B1=E8=B4=A5=E6=97=B6=E4=BF=9D=E7=95=99=E6=97=A7?= =?UTF-8?q?=E7=BA=BF=E5=B9=B6=E6=B8=85=E7=90=86=E5=8D=8A=E6=88=90=E5=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 93 +++++++++------ .../freecad_exchange_auto_routing_test.py | 108 ++++++++++++++++++ 2 files changed, 169 insertions(+), 32 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index bba9eb4..fce56d0 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -926,8 +926,8 @@ def _detach_object_from_groups(doc, obj): pass -def _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=""): - removed = 0 +def _matching_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=""): + matches = [] for obj in list(WiringObjects.iter_routed_wire_objects(doc)): if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": continue @@ -945,6 +945,13 @@ def _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid="" ) if not same_direction and not reverse_direction: continue + matches.append(obj) + return matches + + +def _remove_routing_connection_objects(doc, objects): + removed = 0 + for obj in list(objects or []): try: _detach_object_from_groups(doc, obj) doc.removeObject(obj.Name) @@ -954,6 +961,13 @@ def _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid="" return removed +def _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=""): + return _remove_routing_connection_objects( + doc, + _matching_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=wire_uuid), + ) + + def _find_task_by_wire_uuid(doc, wire_uuid): if not wire_uuid: return None @@ -1051,42 +1065,57 @@ def route_eplan_connection_between_terminals( collisions = detect_collisions(points, obstacles, ignored_segment_indices=ignored_collision_segments) status = "CollisionWarning" if collisions else "Routed" + existing_replacements = [] if opts.get("replace_existing", True): - _remove_existing_routing_connections(doc, start_uuid, end_uuid, wire_uuid=wire_uuid) + existing_replacements = _matching_existing_routing_connections( + doc, + start_uuid, + end_uuid, + wire_uuid=wire_uuid, + ) wire_name = _unique_name(doc, _wire_object_name(start_terminal, end_terminal, wire_uuid)) - wire = _create_wire_geometry(doc, wire_name, points) - wire.Label = wire_label or wire_mark or wire_uuid or "QET Routed Connection" - WiringObjects.set_routed_wire_semantics( - wire, - project_uuid, - wire_uuid, - wire_label or wire_mark or wire_uuid, - start_uuid, - end_uuid, - (getattr(start_terminal, "QetInstanceId", "") or "").strip(), - (getattr(end_terminal, "QetInstanceId", "") or "").strip(), - route_type="RoutedConnection", - route_status=status, - route_mode="EplanRoute", - net_uuid=net_uuid, - group_uuid=group_uuid, - wire_mark=wire_mark, - wire_mark_is_manual=wire_mark_is_manual, - ) - _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id=effective_wire_style_id) - - routed_group = WiringObjects.ensure_routed_group(doc, project_uuid) - if wire not in getattr(routed_group, "Group", []): - routed_group.addObject(wire) + wire = None try: - routed_group.ViewObject.Visibility = True + wire = _create_wire_geometry(doc, wire_name, points) + wire.Label = wire_label or wire_mark or wire_uuid or "QET Routed Connection" + WiringObjects.set_routed_wire_semantics( + wire, + project_uuid, + wire_uuid, + wire_label or wire_mark or wire_uuid, + start_uuid, + end_uuid, + (getattr(start_terminal, "QetInstanceId", "") or "").strip(), + (getattr(end_terminal, "QetInstanceId", "") or "").strip(), + route_type="RoutedConnection", + route_status=status, + route_mode="EplanRoute", + net_uuid=net_uuid, + group_uuid=group_uuid, + wire_mark=wire_mark, + wire_mark_is_manual=wire_mark_is_manual, + ) + _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id=effective_wire_style_id) + + routed_group = WiringObjects.ensure_routed_group(doc, project_uuid) + if wire not in getattr(routed_group, "Group", []): + routed_group.addObject(wire) + try: + routed_group.ViewObject.Visibility = True + except Exception: + pass + _style_wire(wire, collision_count=len(collisions)) + + task = _find_task_by_wire_uuid(doc, wire_uuid) + _set_task_status(task, status) except Exception: - pass - _style_wire(wire, collision_count=len(collisions)) + if wire is not None: + _remove_routing_connection_objects(doc, [wire]) + raise - task = _find_task_by_wire_uuid(doc, wire_uuid) - _set_task_status(task, status) + if existing_replacements: + _remove_routing_connection_objects(doc, existing_replacements) try: doc.recompute() diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index cb39904..f22aca8 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -367,6 +367,7 @@ class AutoRoutingTest(unittest.TestCase): 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)) @@ -396,6 +397,113 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual([first], routed_wires) self.assertIsNotNone(doc.getObject(first.Name)) + def test_eplan_connection_route_keeps_existing_wire_when_new_geometry_creation_fails(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + first = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + )["wire"] + original_create_wire_geometry = auto_routing._create_wire_geometry + + def failing_create_wire_geometry(_doc, _name, _points): + raise RuntimeError("create failed") + + auto_routing._create_wire_geometry = failing_create_wire_geometry + try: + with self.assertRaises(RuntimeError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + finally: + auto_routing._create_wire_geometry = original_create_wire_geometry + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual([first], routed_wires) + self.assertIsNotNone(doc.getObject(first.Name)) + + def test_eplan_connection_route_cleans_up_half_created_wire_when_draft_fallback_fails(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + first = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + )["wire"] + part_module = sys.modules["Part"] + draft_module = sys.modules.get("Draft") + if draft_module is None: + draft_module = types.ModuleType("Draft") + sys.modules["Draft"] = draft_module + original_make_polygon = part_module.makePolygon + original_make_wire = getattr(draft_module, "make_wire", None) + original_add_object = doc.addObject + + def failing_make_polygon(*args, **kwargs): + raise RuntimeError("part unavailable") + + def half_created_make_wire(points, closed=False, placement=None, face=None, support=None, bs2wire=False): + obj = doc.addObject("Part::FeaturePython", "Wire") + obj.Points = list(points) + raise RuntimeError("draft failed") + + def failing_add_object(type_name, name): + if type_name == "App::FeaturePython": + raise RuntimeError("fallback failed") + return original_add_object(type_name, name) + + part_module.makePolygon = failing_make_polygon + draft_module.make_wire = half_created_make_wire + doc.addObject = failing_add_object + try: + with self.assertRaises(RuntimeError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + finally: + part_module.makePolygon = original_make_polygon + if original_make_wire is None: + delattr(draft_module, "make_wire") + else: + draft_module.make_wire = original_make_wire + doc.addObject = original_add_object + + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual([first], routed_wires) + self.assertEqual(0, len([obj for obj in doc.Objects if obj.Name == "Wire"])) + def test_route_carrier_styles_make_generated_objects_distinguishable(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() From af660af3390c166f640c71e46bb261c12e1d5e01 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:11:24 +0800 Subject: [PATCH 14/63] =?UTF-8?q?fix:=20=E5=AE=8C=E5=96=84=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E6=9B=BF=E6=8D=A2=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E5=9B=9E=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 6 ++- .../freecad_exchange_auto_routing_test.py | 43 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index fce56d0..9437829 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1115,7 +1115,11 @@ def route_eplan_connection_between_terminals( raise if existing_replacements: - _remove_routing_connection_objects(doc, existing_replacements) + removed_existing = _remove_routing_connection_objects(doc, existing_replacements) + if removed_existing != len(existing_replacements): + if wire is not None: + _remove_routing_connection_objects(doc, [wire]) + raise AutoRoutingError("Failed to replace existing routed connection.") try: doc.recompute() diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index f22aca8..bc77a15 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -504,6 +504,49 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual([first], routed_wires) self.assertEqual(0, len([obj for obj in doc.Objects if obj.Name == "Wire"])) + def test_eplan_connection_route_keeps_existing_wire_when_old_replacement_removal_fails(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + first = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + )["wire"] + original_remove = auto_routing._remove_routing_connection_objects + + def failing_remove(target_doc, objects): + if first in list(objects or []): + return 0 + return original_remove(target_doc, objects) + + auto_routing._remove_routing_connection_objects = failing_remove + try: + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + finally: + auto_routing._remove_routing_connection_objects = original_remove + + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual([first], routed_wires) + def test_route_carrier_styles_make_generated_objects_distinguishable(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() From 1802ef5992c39c5e4eeffd125a9eb6daeb1541aa Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:18:55 +0800 Subject: [PATCH 15/63] =?UTF-8?q?feat:=20=E6=98=BE=E7=A4=BA=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E8=B7=AF=E5=BE=84=E7=BD=91=E7=BB=9C=E6=96=AD=E7=82=B9?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 18 ++++++++++++++- .../freecad_exchange_auto_routing_test.py | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 9437829..5f15cc1 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1622,6 +1622,19 @@ def _format_distance_mm(value): return "未知距离" +def _format_point_text(point): + if not isinstance(point, dict): + return "未知位置" + try: + return "({0:.1f}, {1:.1f}, {2:.1f})".format( + float(point.get("x", 0.0)), + float(point.get("y", 0.0)), + float(point.get("z", 0.0)), + ) + except Exception: + return "未知位置" + + def _diagnostic_terminal_text(item): if not isinstance(item, dict): return "未知端子" @@ -1667,7 +1680,10 @@ def format_routing_path_network_report(diagnostic): sample = possible_breaks[0] carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} carrier_text = carrier.get("label") or carrier.get("name") or "未知线槽" - message += "\n线槽端点疑似断开:{0}。请补齐相邻线槽、开口或辅助路径。".format(carrier_text) + message += "\n线槽端点疑似断开:{0} @ {1}。请补齐相邻线槽、开口或辅助路径。".format( + carrier_text, + _format_point_text(sample.get("point")), + ) isolated = _dict_items(diagnostic.get("isolated_components", []) or []) if isolated: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index bc77a15..c7c6dde 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1033,6 +1033,28 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("布线路径网络检查发现", message) self.assertIn("首个问题:external_issue", message) + def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="线槽A", + project_uuid="project-1", + kind="WireDuct", + ) + + 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_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() From c301c2c4bc648c2236b090b4ed8cada3b2cc06b9 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:24:58 +0800 Subject: [PATCH 16/63] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=BC=BA=E5=A4=B1=E7=AB=AF=E5=AD=90?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 21 +++++++++++++++-- .../freecad_exchange_auto_routing_test.py | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 5f15cc1..caab10e 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1354,6 +1354,23 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la return report +def _missing_endpoint_label(sample, side): + terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() + terminal_display = str(sample.get("{0}_terminal_display".format(side), "") or "").strip() + if element_uuid and terminal_display: + label = "{0}/{1}".format(element_uuid, terminal_display) + elif terminal_display: + label = terminal_display + elif element_uuid: + label = element_uuid + else: + return terminal_uuid + if terminal_uuid and terminal_uuid != label: + return "{0} ({1})".format(label, terminal_uuid) + return label + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1441,8 +1458,8 @@ def format_eplan_connection_route_report(report): sample = (report.get("missing_endpoint_samples") or [None])[0] if sample: message += "\n缺失示例:{0} -> {1}".format( - sample.get("start_terminal_uuid", ""), - sample.get("end_terminal_uuid", ""), + _missing_endpoint_label(sample, "start"), + _missing_endpoint_label(sample, "end"), ) return message diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index c7c6dde..be90d14 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1825,6 +1825,29 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) self.assertIn("缺失示例:terminal-a -> terminal-b", message) + def test_route_eplan_connections_report_includes_readable_missing_endpoint_labels(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 0, + "collision_warnings": 0, + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ + { + "start_terminal_uuid": "terminal-a", + "start_element_uuid": "device-a", + "start_terminal_display": "A1", + "end_terminal_uuid": "terminal-b", + "end_element_uuid": "device-b", + "end_terminal_display": "B1", + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("缺失示例:device-a/A1 (terminal-a) -> device-b/B1 (terminal-b)", message) + def test_route_eplan_connections_report_calls_out_clearance_collision_kind(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() From b5ee5c9c01449cb6b79342a83b523357edaf921c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:27:11 +0800 Subject: [PATCH 17/63] =?UTF-8?q?feat:=20=E6=A0=87=E6=98=8E=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=BC=BA=E5=A4=B1=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=E6=96=B9=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 16 +++++++++++++- .../freecad_exchange_auto_routing_test.py | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index caab10e..686e27a 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1371,6 +1371,19 @@ def _missing_endpoint_label(sample, side): return label +def _missing_endpoint_side_summary(sample): + missing_sides = [] + if sample.get("start_found") is False: + missing_sides.append("起点") + if sample.get("end_found") is False: + missing_sides.append("终点") + if not missing_sides: + return "" + if len(missing_sides) == 2: + return "(缺失:两端)" + return "(缺失:{0})".format(missing_sides[0]) + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1457,9 +1470,10 @@ def format_eplan_connection_route_report(report): message += " 请先从 QET 重新导入/更新工程端子,使端子 UUID 不再是 local:...。" sample = (report.get("missing_endpoint_samples") or [None])[0] if sample: - message += "\n缺失示例:{0} -> {1}".format( + message += "\n缺失示例:{0} -> {1}{2}".format( _missing_endpoint_label(sample, "start"), _missing_endpoint_label(sample, "end"), + _missing_endpoint_side_summary(sample), ) return message diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index be90d14..0573821 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1848,6 +1848,28 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("缺失示例:device-a/A1 (terminal-a) -> device-b/B1 (terminal-b)", message) + def test_route_eplan_connections_report_identifies_which_endpoint_is_missing(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 0, + "collision_warnings": 0, + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ + { + "start_terminal_uuid": "terminal-start", + "start_found": True, + "end_terminal_uuid": "terminal-missing", + "end_found": False, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("缺失示例:terminal-start -> terminal-missing", message) + self.assertIn("缺失:终点", message) + def test_route_eplan_connections_report_calls_out_clearance_collision_kind(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() From 9422fcc14923a20565226093b684db48a21f5222 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:42:16 +0800 Subject: [PATCH 18/63] =?UTF-8?q?feat:=20=E4=BF=9D=E7=95=99=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E7=AB=AF=E7=82=B9=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 87 +++++++++++++++++-- .../freecad_exchange_auto_routing_test.py | 81 +++++++++++++++++ 2 files changed, 163 insertions(+), 5 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 686e27a..c032e51 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -553,9 +553,46 @@ def _set_string(obj, name, value, description="Routing connection property"): TerminalObjects.ensure_string_property(obj, name, "QET Routing", description, value) -def _route_payload(route_data, collisions, wire_style_id=""): +def _clean_endpoint_metadata(endpoint_metadata): + if not isinstance(endpoint_metadata, dict): + return {} + allowed = ( + "start_element_uuid", + "start_terminal_display", + "start_device_label", + "end_element_uuid", + "end_terminal_display", + "end_device_label", + "endpoint_label", + ) + cleaned = {} + for key in allowed: + value = str(endpoint_metadata.get(key, "") or "").strip() + if value: + cleaned[key] = value + return cleaned + + +def _set_endpoint_metadata(wire, endpoint_metadata): + metadata = _clean_endpoint_metadata(endpoint_metadata) + property_names = { + "start_element_uuid": "QetStartElementUuid", + "start_terminal_display": "QetStartTerminalDisplay", + "start_device_label": "QetStartDeviceLabel", + "end_element_uuid": "QetEndElementUuid", + "end_terminal_display": "QetEndTerminalDisplay", + "end_device_label": "QetEndDeviceLabel", + "endpoint_label": "QetEndpointLabel", + } + for key, prop_name in property_names.items(): + if key in metadata: + _set_string(wire, prop_name, metadata[key], "QET routed wire endpoint metadata") + return metadata + + +def _route_payload(route_data, collisions, wire_style_id="", endpoint_metadata=None): points = route_data.get("points", []) - return { + payload = { "algorithm": route_data.get("algorithm", ""), "length_mm": _route_length(points), "wire_style_id": str(wire_style_id or "").strip(), @@ -566,10 +603,15 @@ def _route_payload(route_data, collisions, wire_style_id=""): "network": route_data.get("network", {}), "route_track": route_data.get("route_track", {}), } + metadata = _clean_endpoint_metadata(endpoint_metadata) + if metadata: + payload["endpoint_metadata"] = metadata + return payload -def _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id=""): +def _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id="", endpoint_metadata=None): length_mm = _route_length(route_data.get("points", [])) + cleaned_endpoint_metadata = _set_endpoint_metadata(wire, endpoint_metadata) _set_string( wire, "QetRouteAlgorithm", @@ -591,7 +633,15 @@ def _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id _set_string( wire, "QetRouteDiagnosticsJson", - json.dumps(_route_payload(route_data, collisions, wire_style_id=wire_style_id), ensure_ascii=False), + json.dumps( + _route_payload( + route_data, + collisions, + wire_style_id=wire_style_id, + endpoint_metadata=cleaned_endpoint_metadata, + ), + ensure_ascii=False, + ), "Routing connection diagnostics", ) if route_data.get("network"): @@ -1024,6 +1074,7 @@ def route_eplan_connection_between_terminals( wire_mark="", wire_mark_is_manual=False, wire_style_id="", + endpoint_metadata=None, ): if doc is None: raise AutoRoutingError("No FreeCAD document is available.") @@ -1096,7 +1147,13 @@ def route_eplan_connection_between_terminals( wire_mark=wire_mark, wire_mark_is_manual=wire_mark_is_manual, ) - _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id=effective_wire_style_id) + _set_routing_connection_metadata( + wire, + route_data, + collisions, + wire_style_id=effective_wire_style_id, + endpoint_metadata=endpoint_metadata, + ) routed_group = WiringObjects.ensure_routed_group(doc, project_uuid) if wire not in getattr(routed_group, "Group", []): @@ -1282,6 +1339,15 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la lane_key = _route_lane_key(start_uuid, end_uuid) route_lane_index = lane_indexes_by_pair.get(lane_key, 0) try: + endpoint_metadata = { + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label"), + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + } result = route_eplan_connection_between_terminals( doc, start_terminal, @@ -1295,6 +1361,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la wire_mark=_wire_item_value(item, "wire_mark"), wire_mark_is_manual=bool(item.get("wire_mark_is_manual", False)), wire_style_id=_wire_item_value(item, "wire_style_id"), + endpoint_metadata=endpoint_metadata, ) except Exception as exc: error_text = str(exc) @@ -1338,7 +1405,14 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), "wire_style_id": _wire_item_value(item, "wire_style_id"), "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label"), "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), "algorithm": result["algorithm"], "route_status": result["route_status"], "length_mm": route_length, @@ -1561,10 +1635,13 @@ def _wire_tasks_payload(doc): "start_instance_id": (getattr(task, "QetStartInstanceId", "") or "").strip(), "start_terminal_uuid": (getattr(task, "QetStartTerminalUuid", "") or "").strip(), "start_terminal_display": (getattr(task, "QetStartTerminalDisplay", "") or "").strip(), + "start_device_label": (getattr(task, "QetStartDeviceLabel", "") or "").strip(), "end_element_uuid": (getattr(task, "QetEndElementUuid", "") or "").strip(), "end_instance_id": (getattr(task, "QetEndInstanceId", "") or "").strip(), "end_terminal_uuid": (getattr(task, "QetEndTerminalUuid", "") or "").strip(), "end_terminal_display": (getattr(task, "QetEndTerminalDisplay", "") or "").strip(), + "end_device_label": (getattr(task, "QetEndDeviceLabel", "") or "").strip(), + "endpoint_label": (getattr(task, "QetEndpointLabel", "") or "").strip(), } ) return payload diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 0573821..1d9581c 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1572,6 +1572,87 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, route["network"]["carriers"]) self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + def test_route_eplan_connections_preserves_endpoint_metadata_on_routed_wire(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_element_uuid": "device-a", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "A1", + "end_element_uuid": "device-b", + "end_terminal_uuid": "terminal-end", + "end_terminal_display": "B1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = routed_group.Group[0] + diagnostics = json.loads(wire.QetRouteDiagnosticsJson) + + self.assertEqual("device-a", wire.QetStartElementUuid) + self.assertEqual("A1", wire.QetStartTerminalDisplay) + self.assertEqual("device-b", wire.QetEndElementUuid) + self.assertEqual("B1", wire.QetEndTerminalDisplay) + self.assertEqual("device-a", report["routes"][0]["start_element_uuid"]) + self.assertEqual("B1", report["routes"][0]["end_terminal_display"]) + self.assertEqual("A1", diagnostics["endpoint_metadata"]["start_terminal_display"]) + + def test_route_eplan_connection_tasks_preserve_task_endpoint_labels_on_routed_wire(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", + ) + task = wiring_objects.create_wire_task( + doc, + "project-1", + "wire-1", + "N4111", + "terminal-start", + "terminal-end", + "instance-a", + "instance-b", + ) + terminal_objects.ensure_string_property(task, "QetStartDeviceLabel", "QET Wiring", "", "QF1") + terminal_objects.ensure_string_property(task, "QetEndDeviceLabel", "QET Wiring", "", "X1") + terminal_objects.ensure_string_property(task, "QetEndpointLabel", "QET Wiring", "", "QF1:A1 -> X1:B1") + + report = auto_routing.route_eplan_connection_tasks(doc) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = routed_group.Group[0] + diagnostics = json.loads(wire.QetRouteDiagnosticsJson) + + self.assertEqual("QF1", wire.QetStartDeviceLabel) + self.assertEqual("X1", wire.QetEndDeviceLabel) + self.assertEqual("QF1:A1 -> X1:B1", wire.QetEndpointLabel) + self.assertEqual("QF1:A1 -> X1:B1", report["routes"][0]["endpoint_label"]) + self.assertEqual("QF1", diagnostics["endpoint_metadata"]["start_device_label"]) + def test_route_eplan_connections_records_wire_identity_for_errors(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 088302771a6aa8236b1c8e6b77e1d689d18c3757 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:49:38 +0800 Subject: [PATCH 19/63] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E8=B7=AF=E7=94=B1=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 32 +++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 36 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index c032e51..3b39765 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1373,7 +1373,14 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label"), "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), "error": error_text, } ) @@ -1458,6 +1465,24 @@ def _missing_endpoint_side_summary(sample): return "(缺失:{0})".format(missing_sides[0]) +def _wire_sample_text(sample): + return ( + str(sample.get("wire_label", "") or "").strip() + or str(sample.get("wire_uuid", "") or "").strip() + or "未知导线" + ) + + +def _endpoint_pair_text(sample): + endpoint_label = str(sample.get("endpoint_label", "") or "").strip() + if endpoint_label: + return endpoint_label + return "{0} -> {1}".format( + _missing_endpoint_label(sample, "start"), + _missing_endpoint_label(sample, "end"), + ) + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1503,6 +1528,13 @@ def format_eplan_connection_route_report(report): errors = report.get("errors", []) or [] if errors: message += "\n首个错误:{0}".format(str(errors[0])) + error_sample = (report.get("error_samples") or [None])[0] + if error_sample: + message += "\n错误示例:导线 {0},{1}:{2}".format( + _wire_sample_text(error_sample), + _endpoint_pair_text(error_sample), + error_sample.get("error", ""), + ) collision_sample = (report.get("collision_samples") or [None])[0] if collision_sample: obstacle_text = ( diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 1d9581c..c94c519 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1688,6 +1688,42 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("terminal-start", report["error_samples"][0]["end_terminal_uuid"]) self.assertIn("different", report["error_samples"][0]["error"]) + def test_route_eplan_connections_report_includes_readable_error_sample(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, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-bad", + "wire_label": "N500", + "start_element_uuid": "device-a", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "A1", + "end_element_uuid": "device-a", + "end_terminal_uuid": "terminal-start", + "end_terminal_display": "A1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual("device-a", report["error_samples"][0]["start_element_uuid"]) + self.assertIn("错误示例:导线 N500", message) + self.assertIn("device-a/A1 (terminal-start) -> device-a/A1 (terminal-start)", message) + def test_route_eplan_connections_counts_route_statuses_for_summary(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 967ad4b7f1d19bd1b41a0c4e1bc9bc849aeab5af Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 17:58:17 +0800 Subject: [PATCH 20/63] =?UTF-8?q?feat:=20=E7=AE=80=E5=8C=96=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E5=85=B1=E7=BA=BF=E5=86=97=E4=BD=99?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 77 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 33 ++++++++ 2 files changed, 110 insertions(+) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 3b39765..2da8cad 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -206,6 +206,79 @@ def _append_orthogonal(points, target_point, preferred_axis=None): _append_unique(points, point) +def _collinear_points(first, middle, last): + ax = float(middle.x) - float(first.x) + ay = float(middle.y) - float(first.y) + az = float(middle.z) - float(first.z) + bx = float(last.x) - float(middle.x) + by = float(last.y) - float(middle.y) + bz = float(last.z) - float(middle.z) + cross_x = ay * bz - az * by + cross_y = az * bx - ax * bz + cross_z = ax * by - ay * bx + dot = ax * bx + ay * by + az * bz + return ( + abs(cross_x) <= 0.000001 + and abs(cross_y) <= 0.000001 + and abs(cross_z) <= 0.000001 + and dot >= -0.000001 + ) + + +def _route_point_key(point, tolerance=0.001): + scale = 1.0 / float(tolerance or 0.001) + return ( + int(round(float(point.x) * scale)), + int(round(float(point.y) * scale)), + int(round(float(point.z) * scale)), + ) + + +def _simplify_collinear_points(points, preserved_point_keys=None): + normalized = [_vector(point) for point in points or [] if _is_finite_point(_vector(point))] + if len(normalized) <= 2: + return normalized + preserved_indices = {0, 1, len(normalized) - 2, len(normalized) - 1} + preserved_point_keys = set(preserved_point_keys or []) + simplified = [normalized[0]] + simplified_indices = [0] + for index, point in enumerate(normalized[1:], start=1): + _append_unique(simplified, point) + if len(simplified_indices) < len(simplified): + simplified_indices.append(index) + while len(simplified) >= 3 and _collinear_points( + simplified[-3], + simplified[-2], + simplified[-1], + ): + if ( + simplified_indices[-2] in preserved_indices + or _route_point_key(simplified[-2]) in preserved_point_keys + ): + break + simplified.pop(-2) + simplified_indices.pop(-2) + return simplified + + +def _important_route_node_keys(network, path_keys, path_result): + edges = network.get("edges", {}) if isinstance(network, dict) else {} + important = { + key + for key in path_keys or [] + if len(edges.get(key, []) or []) != 2 + } + segments = path_result.get("segments", []) if isinstance(path_result, dict) else [] + for index in range(1, len(path_keys or []) - 1): + previous_segment = segments[index - 1] if index - 1 < len(segments) else {} + next_segment = segments[index] if index < len(segments) else {} + previous_carrier = (previous_segment.get("carrier") or {}).get("name", "") + next_carrier = (next_segment.get("carrier") or {}).get("name", "") + if previous_carrier != next_carrier: + important.add(path_keys[index]) + return important + + def _offset(point, direction, distance): return App.Vector( float(point.x) + float(direction.x) * float(distance), @@ -716,6 +789,10 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non _append_unique(points, point) _append_orthogonal(points, end_exit) _append_unique(points, end_origin) + points = _simplify_collinear_points( + points, + preserved_point_keys=_important_route_node_keys(network, path_keys, path_result), + ) return { "algorithm": "network-dijkstra-v1", diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index c94c519..776fb89 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -332,6 +332,39 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(2, third_payload["lane"]["index"]) self.assertEqual(-12.0, third_payload["lane"]["offset_mm"]) + def test_network_eplan_connection_route_removes_collinear_network_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") + 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(50, 0, 20), + app.Vector(100, 0, 20), + ], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + point_tuples = [(point.x, point.y, point.z) for point in result["points"]] + self.assertNotIn((50.0, 0.0, 20.0), point_tuples) + self.assertEqual( + [ + (0.0, 0.0, 0.0), + (0.0, 0.0, 20.0), + (100.0, 0.0, 20.0), + (100.0, 0.0, 0.0), + ], + point_tuples, + ) + def test_eplan_connection_route_replaces_existing_wire_uuid_when_endpoints_change(self): _install_fake_freecad() terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() From a225104a518ceddb7cb00c52177c699a3206230c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 18:04:00 +0800 Subject: [PATCH 21/63] =?UTF-8?q?feat:=20=E6=8C=89=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=AE=B5=E9=94=99=E5=BC=80=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=B8=83=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 90 ++++++++++++++++--- .../freecad_exchange_auto_routing_test.py | 48 ++++++++++ 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 2da8cad..83af255 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1296,6 +1296,32 @@ def _route_lane_key(start_uuid, end_uuid): return tuple(endpoints) +def _route_segment_key(segment): + if not isinstance(segment, dict): + return None + carrier = segment.get("carrier") or {} + carrier_name = str(carrier.get("name", "") or "").strip() + from_key = tuple(segment.get("from_key", []) or []) + to_key = tuple(segment.get("to_key", []) or []) + if not from_key or not to_key: + return None + return ( + carrier_name, + tuple(sorted((from_key, to_key))), + ) + + +def _route_segment_keys(result): + route_track = result.get("route_track", {}) if isinstance(result, dict) else {} + segments = route_track.get("segments", []) if isinstance(route_track, dict) else [] + keys = [] + for segment in segments or []: + key = _route_segment_key(segment) + if key is not None: + keys.append(key) + return keys + + def bind_wire_task_terminals_from_payload(doc, payload): """Bind local template terminals to QET terminal UUIDs without creating wires.""" if doc is None: @@ -1376,11 +1402,29 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la report["prepared_layout"] = prepared_layout missing_endpoint_uuids = set() lane_indexes_by_pair = {} + lane_indexes_by_segment = {} def add_status(status): key = str(status or "").strip() or "Unknown" report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 + def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): + return route_eplan_connection_between_terminals( + doc, + start_terminal, + end_terminal, + route_index=route_lane_index, + options=options, + wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), + wire_label=_wire_item_value(item, "wire_label", "wire_mark"), + net_uuid=_wire_item_value(item, "net_uuid"), + group_uuid=_wire_item_value(item, "group_uuid"), + wire_mark=_wire_item_value(item, "wire_mark"), + wire_mark_is_manual=bool(item.get("wire_mark_is_manual", False)), + wire_style_id=_wire_item_value(item, "wire_style_id"), + endpoint_metadata=endpoint_metadata, + ) + for item in wires: if not isinstance(item, dict): report["skipped_invalid"] += 1 @@ -1425,21 +1469,33 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "end_device_label": _wire_item_value(item, "end_device_label"), "endpoint_label": _wire_item_value(item, "endpoint_label"), } - result = route_eplan_connection_between_terminals( - doc, + result = create_route( + route_lane_index, + item, start_terminal, end_terminal, - route_index=route_lane_index, - options=options, - wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), - wire_label=_wire_item_value(item, "wire_label", "wire_mark"), - net_uuid=_wire_item_value(item, "net_uuid"), - group_uuid=_wire_item_value(item, "group_uuid"), - wire_mark=_wire_item_value(item, "wire_mark"), - wire_mark_is_manual=bool(item.get("wire_mark_is_manual", False)), - wire_style_id=_wire_item_value(item, "wire_style_id"), - endpoint_metadata=endpoint_metadata, + endpoint_metadata, ) + route_segment_keys = _route_segment_keys(result) + shared_lane_index = max( + [lane_indexes_by_segment.get(key, 0) for key in route_segment_keys] or [0] + ) + final_lane_index = max(route_lane_index, shared_lane_index) + if final_lane_index != route_lane_index: + initial_wire = result.get("wire") if isinstance(result, dict) else None + try: + result = create_route( + final_lane_index, + item, + start_terminal, + end_terminal, + endpoint_metadata, + ) + except Exception: + if initial_wire is not None: + _remove_routing_connection_objects(doc, [initial_wire]) + raise + route_segment_keys = _route_segment_keys(result) except Exception as exc: error_text = str(exc) report["errors"].append(error_text) @@ -1462,7 +1518,15 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } ) continue - lane_indexes_by_pair[lane_key] = route_lane_index + 1 + lane_indexes_by_pair[lane_key] = max( + lane_indexes_by_pair.get(lane_key, 0), + int(result.get("lane", {}).get("index", 0) or 0) + 1, + ) + for segment_key in route_segment_keys: + lane_indexes_by_segment[segment_key] = max( + lane_indexes_by_segment.get(segment_key, 0), + int(result.get("lane", {}).get("index", 0) or 0) + 1, + ) if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 add_status(result["route_status"]) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 776fb89..43d8aaf 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1870,6 +1870,54 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(0, report["routes"][1]["lane"]["index"]) self.assertEqual(1, report["routes"][2]["lane"]["index"]) + def test_route_eplan_connections_lane_index_increments_for_shared_route_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, "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)], + 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, "lane_axis": "y"}, + ) + + self.assertEqual(0, report["routes"][0]["lane"]["index"]) + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + routed_group = doc.getObject("QETWiring_04_Routed") + second_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "wire-b" + ][0] + self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 711697b5fa3670a53bb2dd6074895961ab7259ed Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 14:18:58 +0800 Subject: [PATCH 22/63] fix: simplify wire duct model asset --- data/examples/qet_cabinet_assets/README.md | 2 +- .../create_qet_cabinet_assets.py | 54 +- .../qet_cabinet_assets_report.json | 46 +- .../qet_cabinet_assets/qet_din_rail.FCStd | Bin 13934 -> 13933 bytes .../qet_cabinet_assets/qet_din_rail.step | 32 +- .../qet_cabinet_assets/qet_wire_duct.FCStd | Bin 41012 -> 27749 bytes .../qet_cabinet_assets/qet_wire_duct.step | 12505 +++++++--------- 7 files changed, 5345 insertions(+), 7294 deletions(-) diff --git a/data/examples/qet_cabinet_assets/README.md b/data/examples/qet_cabinet_assets/README.md index 891193f..5de79e2 100644 --- a/data/examples/qet_cabinet_assets/README.md +++ b/data/examples/qet_cabinet_assets/README.md @@ -19,7 +19,7 @@ The wire duct is a gray open duct for cabinet routing: - Width: `40 mm` - Height: `40 mm` -It includes a base plate, two side walls, comb-style side slots, and mounting hole markers. +It is generated as one FreeCAD tree object, `WireDuct_Body`, with the comb-style side slots and mounting holes cut into the body. ## DIN Rail diff --git a/data/examples/qet_cabinet_assets/create_qet_cabinet_assets.py b/data/examples/qet_cabinet_assets/create_qet_cabinet_assets.py index 682fa57..cf86109 100644 --- a/data/examples/qet_cabinet_assets/create_qet_cabinet_assets.py +++ b/data/examples/qet_cabinet_assets/create_qet_cabinet_assets.py @@ -113,60 +113,60 @@ def _create_wire_duct(): y0 = -width / 2.0 light_gray = (0.72, 0.74, 0.74) - dark_gray = (0.18, 0.2, 0.22) - objects = [ - _box(doc, "WireDuct_BasePlate", length, width, wall, x0, y0, 0.0, light_gray), - _box(doc, "WireDuct_LeftWall", length, wall, height, x0, y0, 0.0, light_gray), - _box(doc, "WireDuct_RightWall", length, wall, height, x0, width / 2.0 - wall, 0.0, light_gray), - ] + base = Part.makeBox(length, width, wall, App.Vector(x0, y0, 0.0)) + left_wall = Part.makeBox(length, wall, height, App.Vector(x0, y0, 0.0)) + right_wall = Part.makeBox(length, wall, height, App.Vector(x0, width / 2.0 - wall, 0.0)) + body = base.fuse(left_wall).fuse(right_wall) slot_count = 18 slot_pitch = length / slot_count finger_width = slot_pitch * 0.45 for index in range(slot_count): center_x = x0 + slot_pitch * (index + 0.5) - objects.append( - _box( - doc, - "WireDuct_LeftCombSlot_{0:02d}".format(index + 1), + body = body.cut( + Part.makeBox( finger_width, - wall + 0.2, + wall + 0.4, height - 8.0, - center_x - finger_width / 2.0, - y0 - 0.1, - 8.0, - dark_gray, + App.Vector(center_x - finger_width / 2.0, y0 - 0.2, 8.0), ) ) - objects.append( - _box( - doc, - "WireDuct_RightCombSlot_{0:02d}".format(index + 1), + body = body.cut( + Part.makeBox( finger_width, - wall + 0.2, + wall + 0.4, height - 8.0, - center_x - finger_width / 2.0, - width / 2.0 - wall - 0.1, - 8.0, - dark_gray, + App.Vector(center_x - finger_width / 2.0, width / 2.0 - wall - 0.2, 8.0), ) ) for center_x in (-60.0, 0.0, 60.0): - objects.append(_cylinder_z(doc, "WireDuct_MountHole_{0:g}".format(center_x), 2.2, wall + 0.2, center_x, 0.0, 0.0, dark_gray)) + body = body.cut( + Part.makeCylinder( + 2.2, + wall + 0.4, + App.Vector(center_x, 0.0, -0.2), + App.Vector(0, 0, 1), + ) + ) + + body_obj = doc.addObject("Part::Feature", "WireDuct_Body") + body_obj.Shape = body + _style(body_obj, light_gray, 0) doc.recompute() fcstd = OUT_DIR / "qet_wire_duct.FCStd" step = OUT_DIR / "qet_wire_duct.step" doc.saveAs(str(fcstd)) - _export_step(objects, step) + _export_step([body_obj], step) return { "name": "wire_duct", "fcstd": str(fcstd), "step": str(step), "dimensions_mm": {"length": length, "width": width, "height": height}, - "objects": [obj.Name for obj in objects], + "objects": [body_obj.Name], + "object_count": 1, } diff --git a/data/examples/qet_cabinet_assets/qet_cabinet_assets_report.json b/data/examples/qet_cabinet_assets/qet_cabinet_assets_report.json index 1bb4dfc..ac7dfc0 100644 --- a/data/examples/qet_cabinet_assets/qet_cabinet_assets_report.json +++ b/data/examples/qet_cabinet_assets/qet_cabinet_assets_report.json @@ -10,49 +10,9 @@ "height": 40.0 }, "objects": [ - "WireDuct_BasePlate", - "WireDuct_LeftWall", - "WireDuct_RightWall", - "WireDuct_LeftCombSlot_01", - "WireDuct_RightCombSlot_01", - "WireDuct_LeftCombSlot_02", - "WireDuct_RightCombSlot_02", - "WireDuct_LeftCombSlot_03", - "WireDuct_RightCombSlot_03", - "WireDuct_LeftCombSlot_04", - "WireDuct_RightCombSlot_04", - "WireDuct_LeftCombSlot_05", - "WireDuct_RightCombSlot_05", - "WireDuct_LeftCombSlot_06", - "WireDuct_RightCombSlot_06", - "WireDuct_LeftCombSlot_07", - "WireDuct_RightCombSlot_07", - "WireDuct_LeftCombSlot_08", - "WireDuct_RightCombSlot_08", - "WireDuct_LeftCombSlot_09", - "WireDuct_RightCombSlot_09", - "WireDuct_LeftCombSlot_10", - "WireDuct_RightCombSlot_10", - "WireDuct_LeftCombSlot_11", - "WireDuct_RightCombSlot_11", - "WireDuct_LeftCombSlot_12", - "WireDuct_RightCombSlot_12", - "WireDuct_LeftCombSlot_13", - "WireDuct_RightCombSlot_13", - "WireDuct_LeftCombSlot_14", - "WireDuct_RightCombSlot_14", - "WireDuct_LeftCombSlot_15", - "WireDuct_RightCombSlot_15", - "WireDuct_LeftCombSlot_16", - "WireDuct_RightCombSlot_16", - "WireDuct_LeftCombSlot_17", - "WireDuct_RightCombSlot_17", - "WireDuct_LeftCombSlot_18", - "WireDuct_RightCombSlot_18", - "WireDuct_MountHole__60", - "WireDuct_MountHole_0", - "WireDuct_MountHole_60" - ] + "WireDuct_Body" + ], + "object_count": 1 }, { "name": "din_rail", diff --git a/data/examples/qet_cabinet_assets/qet_din_rail.FCStd b/data/examples/qet_cabinet_assets/qet_din_rail.FCStd index c61ee9bb0a064f5069dc3798de9ac3497142ec65..01973476a073505473b5e15f2e596ba7604a10e2 100644 GIT binary patch delta 2285 zcmZ9Oc|4SBAI8TL#*7)S(TI_KopFSr48qtO71@hXVR|z$vWJ|Zvdi{V=N;=vCQHI$ z5Je(u*|J0;G+G!OBs%1s#_9chdY^xu>+`*@>%M>Y^Uw7x`LX?|_O^Wd;vf)62(%N} z;vpWIE?+1B0u8u;Ku`{~2=ET|Cs9H)ulW11bKM=^Sc!K|Z;88`i0fG1@!2B8LaXJ^ zlQo}b^sPUyx#Myrg_7wKvsP-rKGLPSkg2hyVB6f)E^;M8Xf(1PM`h=~otAk%zp_)~ zOShhl9Ii_Xqq6SSrX&_mNHLev!=f3GFK@qg8YY;4pTP`*!M^#m_Ps9AfSaFe#0|Q! zcUrhyvYnls<-y1jH_XJ+(II~cTAi&Z^IdV0q=gzX#qb*FyO%^s(MD2|i8Ze;Tq(EuGXiV4RT!Hf8v5x+uHWco2n|1qh4)-*VU)J{BTF|uUE9r&pzVE z-i>~J*}`TM*-j*9zb99?VDJz7ZlB5`IOEHNlCNT%;qQC!#os@a4fQc^-ZVpfO{|_| z_rtg2sx%jai4u<9q5qO;iXl4ON4<>ct};1CMb?%QGy;w}ud#9u2n2nKE{G61OrA9s z`|W;qQ_hC1P_nl5gIPp09X6;2(^nO6Rp?^FSYc~DPhz%0)KA*6A&W{Z{44%?#x!E) z<^Cc>{BA*4Uh~d+5?QL+VUAkm@7<@gFQC)$o;P1s!1MEhshTfYjSiKr#f|HKrhH4I zG6M3$!#`9!FW>CDe@0r;mX)vKN-7j=iPITW=JB>fq8rN)cNZ*{D~yc8k)*uX&O{n| zb@*CvF3QWdPb1Ggb+QetF$7k9$XM2X1AUM#{Nl@fpE#8{nE&T${pt7bUnfLg_pDxq zwcUv)im++(#*qyP=AVtfZG3I=iIHrhrc4{0tI6sn3YHcgUxW?FFgG#>-Tgu-1~X>X zQm0D6Eq~N)zjIX7rxiI)GY9j9$u9$T`FFRQu4$J{y`qWoe`)@BIoUO|u6;$+CTM?z z+-p-<*m>{ok~w{qB@yG&X-wZ_6{btWF5T?nkTsd^n8B2=8KiyuZR2uSq&75y3a+oZ z71H%&@M1rTX&z{>?BaouksKAVUGy2r)@Y?T(CQ17C9E9UK1!j15?0w)iBsbZCu568 z6=2opqMQrI)+bUU%?07a`0xRPmdCMk_Vr-Z@Y$&edoSI&JWr2~suLj_f}bkLVTdYh z0#S77#naKF-TSo*d_~vewWgWWi4Yc#@Z5Y|vruF>G$*vlS-s^iDF)=3RT5PM)!y1F zIgDwB%v~ZWOT>M1FLfzg?EgT%Mae88cv{c#ry4nrWr}}reqUa9r|d?}H-l#?Vuymp z+-%>Zwq)NLJFCD{3}5}W)X_&+FwDtdq&_~FJXmMQ1e?dkt8UDUKRsPeRlwgEzi+1w zFL3C$1RLcU8`|gcr@x4)A10Wr^@R~c%X7ti6DMzwTyAPHP7f)tkPam|p-~v41NJ?a zvmW05e7c7LU$4O`HOCAMIXJZ819SO|Mq%vk5FtzpXSTMNru#gq;)qKUbgcyYTO z4-u|2(ZZXvA=`!AeoY zwXME}!EZo!RLm7A8Q~i@J)0-xo8$>4?vJ97d*H4+T_d)WPOI`ZSJ@|wGZGs$x*j#i zuI`*`d-zF&SgwAwP@z<$P~%c)q3go!8-$8$%9ai!-j%a!NMlE8`bG(U{2Yy!p8Lpe zegu>f@^X~FZB#xOiB~hW3*=`JVwThBSAN%z(T|zZin*)%VRKw(JGJa`UxA0@%G)h$ zwj4|jMn3|j>-Xbg>rzb-b^*BSL9C}%WctNq{W$%&DXq8;S|6^KB%QroU;?}Aajc5w zl?WsPAKAb~1?mVcxejXissJ^k)pJ__RhryU(;nC!xE>#6w7Kv>zF7RFEe6;<{PDyy66UW1bqSd%aoPmik~oUx#tToM zcQ~yka3bhEaY0mfPvjwv?1?d?*`AP= zvfL9s2jA`E#;q0Qc({VBRJli_(Fga45oT(S9MqQQ0spT|0c#!Y9|<5z2gTD1{Go%E z{joOvvBT_dUJyt|5d@Ot6z$KaHg*emr=!MAK(Htt44{ft=O#1*lNWY4i9?)(^iMnI zT^s8L1Yj}TL>?B!n~ne;AY_3wEL85tK_aoclR|tTkUl3S`%A1F3CttqfpIL9yZ?ZY z^($eH@yA4RzZhY_DcwU{kL;T>6O4TzP$vHWH**0BbTzp%4CtbGaf$$4R|bISA-G6h z55?=D#6jYK30ep^qX*|=KMsbW|G{wJfgY5Ln>bh#!^NiBU|qf9~tP&h@?T>-_aQ%%D#}6nkrS4p9IAxPYTMjRZae z?Al!@05FaN03XXW3po>U&L^0te(_uolQ8NygBK#tqm*K~f;ipM6y2#nnFpfExt_DN z1gTN+Smz~-=V{l(cYNYL8oqD*tZ(;PHoQ(SHmXW(&2;-x zWA{`Yg^*Dv97-H2=pLVn?6fIt_(AU-Y!Qs7CD_H_POgsLXD$`mO4MD0;Oz6YG={}q zb#!z*;5H@(IeY8~WLMk@8dAP$@Xxz;J~`*|0W~Z@DdX+$-D+#k7XkLy##h9A>+V>m zacg>@-cpf8`Qjr|BO4i27xoizYUm|LtmH=u`|K_K1(->I{s``$aLtM9kZn_%kL2-U z9&d6!2YCoRjT+~dgg-Yh5bCfM(Ewh{A1P8WeS|&cR$hRRHe^e{Dx9+t(a~L0((UPN z(LMao?4rhp{bsT2A2y%QGgnK=P+8O=udIZY)A!U>lE>`7A*cCn>GjhiBJp>&Qc>oV zvLP222j8bw7Vk=8qb?mRRU5i_{9@*d)X&4-h5LdMyfmlWZPd=Tf2DnW)^zbm%k=}& zgA&4Alhk@%HE@if^9V|~!(F5_ARg~57H|XMHc^RLS7i416y6aJ%t_E5lOObM&6oVH zecGcl+(X(aFiMEcFj3Y2T`k(nXyrm@JVpBfO!GpTQwnIrD z*Ed%bfqQ6++$H(vrBYglZW8UTSD0NIm275aCPWJ?FFNHOV}`HzOkw-7f^8>YQyof2Dx) zOs0SIPxU7LvQ+#aNh!FiO17UU$NiwB3+JSjgKJ&LrWR*YgQoBG9!`(QW5=%$QaS{3 zXSPpd`aXPD*4inV+boiMemWq5Eu7i8Nqf0{-ut~b=#Z!_PD>97i~KaxOf9NL_qXsyalwk zjGrP_w=Lv|GRQ8PVO;3FDd5Mf+%bFc)8pj<`_(L)tMSl$Tm4tdO731Ht`vd=8&H>$ zNVa@q;yt=`uvY!#<%YU*y(QfuGq;de;N$|h0rZ5Suqc?+5+kVGF@HHljG}oZp@97x zwc0pvP@wHU6KV~v5cGWnyz?)9+f139{~m z#jcVh+fQ$c^@v;ybEEk6#a9@K#6~s!2G~w}f=E$@FDX>GRolDD#4^OKdfC#@df0r* zPIYB0)lrz9jmK7U^OEPUQ`C*Ip=0j`=N(pW&Q%3JkOeee&_Cil;ETyyqkqQL@BSvMdsc!s~C(cE+e$=2@g$cntyEIUIxu} z3#4z-?F~a+-N(l}v#T1bwfvSCqj6cq<4Fp9oZ?dS*1ouFk>+?|N=g2aIir{^V=AdC;ltnECAySW7C4@ug`p3I}{|7cJZ zryEs}?fb+n0k*I z1qPd>`{w}DM^Y_RGhav4t%ZSMm)iMx$5`5OVuh!-{rg0j#R$!xrgDqu#BhHTQL4ks z!8gWzjPyMFzt@bh88=E8eI!g_#lYiY%SId1ZC8nx%Hj;ETFRkl&tkxy_sQFb;$Bh} zi?_WP28`S&x%9l!dX5aaX1%`)h~#=8l#SF1NoVBJd#JB_Xgu?qPRVSO1PYb6jq@rm z0|75cpC`oW+QhZzO@#3yU&Yn-o4yta-9L?7qq_LD5i5=&N{-I#Y|P-ClR1`EsP!VD zili+o9ee~u-5jB04z41myq%Mwfn~|~^0v9F^j<_}5qDKMc0?1~_3Ud3%27q?VX4t{ zTAQN3yn=rklK|@4fJ&3kyA{hs`I{OPY%4@u6)G$%-BO|X7^4WjFQSx^NJ8PPf5L5< z9Y;ul#FqmUy49x2SeFAyY#f$V6X`da(%3f+_dS(lks?J^|JZ(C?Gn7vP)uvHI3QS9uj30Do}(byCE{K!4wBc!t@a$$OVVi1no69{qh zJ#j|z1LrP|mzQDN75)m9dkz9Ax#t+7CiWZ&Em=11|4ma6kJkD{fFv}6?G2cTMoRyB zH@VP`+kyaqlso_&WKG)NPfgq=IEg;AOYmqT*i=C!ZIxX@Jv4dw2aC{P5fZ=SthFZ2 z4Gh*s?Gm}#2uKPHEQd*hH?;X=eqF>Lw=>4Y4gmTrO!^P33l7e~Wx*{p-|qPgE{i@M zR`6FNnLmPppuLX9Zbo|aqvn-7}%~QgF&!) zfJPYA-CZCC0TD!k#uy2(_`iWhmZgCLQCgCq8%A)qcT=Eq{4}dZH0zV){-`6Q#R@8b Z7P>-kG#UVe0DPE_&rz(I%>X%WI}ZoJXBJ!tr)&tLae)S^m=sBrM&d`X=x|^iDrPs zhq(gx@OpA8X)A3TT8~r6i6(-Lw%j52b8(Krvc_xm#ZxxEzA>uU& zxsiV=Bi5{w@bQZv6P*eHupCRyxbWKYbQ>A9q1JluK_Ga7l1DsU&i4^8aNvCtP=fGn zf#YbP>hCky2Z4|i=4NQNSkj3HnYH+3W(oH+!m@g}g*_?+a~RWVIzpC5Y60?y9!nKM z8L}$?k$X;uEB=sRxF~>T3VH=gK270p=3)j}H<@HVr1w?Lv>K8mG{jMwY;KGPQ$@H5 z;Tbjb^Z3`}HA^}t?z9Y2V(&dk$xHnn?pl6$BVV+zZ25_#3=GA7*0@4)ku)_m)`mus zdCT@M-^sDPyc|xOM3^Vi8ae_Dtl0B9-_9f!c-llI*n+1 zlipceSj3@@NBTZZV3BS=8@gAr(1v~eE;l@{jU(6cHR2~z#pn+k7mSWVeE(Cpvo$|% zX5B4T$0k0%j_sO|O*vX}cQ>1>btSPEw9%r^o1Bp9zuykN8lGoCVb78IHBeS~@laPs z<8+Tra+UUSIr|eHs(#=^- z<6-CppON0x-b^xdmN*I7eIL4ZK$8_xTBY(Uf1M?9untPLIUseag&%1uT+NTOTTj=q zsx%vBe6(f0jY!p)oIY%Se_+z$G}KgnHu z_?*nWJ?e`5>CSfC#%0Sh{BWD7WbX6LYYnnEIDSxC7m|oMb;2}?7Om{K`C-6Q_%NXm zv+H^2k+=j`T^aOyQ!4+r^30fhOO$#LLd}4M=QsE3gIvN8(A&Vifqu)I`9RhUrw6Qa z()c~BD3ID{(eXT%_lS;5i@COAv(7vIk-|5m1BDzIfhS2l4iHUfSd4I)X#m~?R9eOe zJx{Ot6zM(IMm0cNLwmA~@IqSbXLui0$~X%qZbhWN##9t`VT4g#Sm{WXlfYY&rq81y zs$y8eC7JK%{R(O-my-I?Bnbnu->zV8RtP4T*OwC7Y#FT$}oFdv>{wM?M$TXuL#sw%>YRt4%l&Oc%~ zFE_c~K%={>I>7(+hw3YRA4k@fgQIi3=&wtTs-I^S@gU%%R*#=BCQDt+aht!o81~9- zS%Eo~>Bv5Psc2+1H^O~6ysjMhztAal2*ER#1? zZCM!xx9bkZz*1~zuGpth?qq?k#!2tq4{6byd$LUHUB^CKs-A!2MEViAe8~G9O!5EP zIY;+~HrtdyKrCJ$K(K*3r^Z(&Gci{a7XwjyQ%@!pOCtv}CSxauGhdlF`nH7qw--1@ zi!X(u*K$nw=Q;E3Aov0&(ROjG0o$vWg@KRvNv15WVpK^{Bq_`(qfp00YRV%II0#T9 z5zKxs`^0|Fnf?z${;e zFDJzQuYkDsx09}y>!vHAH!mZh!FON8qe(N%r@f(9xQ&g$nZA{y}(u>?=aS<~r^^ovlM|yd)f8 z7Di^Q18)j#`Yp1gj=qF%DJq1Z?yGfm$&M7uM~GnekSP29q7?S2rl=k{ZU1w*fSDl5 zhQE%-^2BTpl6@ZZD+|u!tz`vIlXMOe6n$5fIkHeKu(N^JZ>zmyZaGM!oqOOM^Lp@XP^$zOc^^g)uyY2rIFh@pcg37N?iP z;b-%ga##pyF1i@oZ4QoD{CXa5w;)*%4A%JHSjrWkgZB-z_e{!vzw{L!DOxFHAcfp- zV(B*mT(*u8Z}Nzt_B)B~+OW_7IQes;9V&xkCk1uWkp}KUj)XnmZMV(%6ria# zg^{0(aK79sJ`SD(UGIv~R=Kp-qGtfeX(b<2@%~S$j8K5NuCwgjJM<(7;KQ-CEnRc# zU~`TO>82wBLoWXd`8*N!f_9#4HT6x@4;p*ntUjNl-kOn7eCl#JTqY@hE{6>l=I@F& zN8$~7vXv2Am4@xNL>s!76;YOp7ar<5VYuHNjMyS3@Oeg`?FYi|a}XXO*J$VSh(k}` zY*|iyBw+cwX7Hf4IbH6ydrh~p@9mTCH=~d*1aPlUnp7%8gy1e5o(E;HwF&&62l?zb zfdpQWZ*=-HZGr(U_k+@lS_J<0gS;0TKuaC@Mu%_p#)kzkD4nK-;13w&wg1o3?#r~% zd)e97p>sqPygPG zeX+!S4&*_XK z34gZ>k0_WCH^PAe$(k=O0cqeR$m})@Y{)uGmQbL>{gFcmL@&5(IJvITokI95HOjdl zVXQ;AX8_P3Gj4}7kt^K%?84K_xDQ&=hg!}0T-x_v?B=15*uCQB|10)37vNv)*V*kG z*pN1baB>Qib0Namc;TgQy^rWH_c@5bl-B&&fN9kFvt)XZxFp5f35eq2=j!)40QkaKX3M zmdyu@A9>m01c>YG3ti3lNC48_aXO5tTXGnIB2f6K`(zfzARwUjAS@uQUeZ&6br8Ri z^?m1vdYp-F$#!gUbRZS>O0mvAR+d|wOO1P%^#6;qbEwj0g%xR|=TS>jInk-doUJ|rlN7)eOtS|Kb#M0O^XKdaYVqLsLwnrz|K>mF zwuf%}hI=6VTeh2lF~VHno;5I#1Z7Q)^eb zbH2oBmy!0inBPUt?xSyeB!U3d`_(_$+jPw&2DJ`&e@;IkM`7|AclG98WLYdNt$DyX zIXxKWz$Z4*>piY@At5$d-}sGb)BQN2COF9Z{D}>Ad`L5`mX|^6_mF0jyE7!dgWqv( zT-}6UtgO-v>jGsJl1wr1dbY0HrB@8uI(Cs= zZ#(q?EoYa^;aRrS{+}YOKEFUd|Ew3j0}c%(*Z_yvjAdSar%myI*FwGh8QTWVGjO*% z^1WuhUkXMIU_4WJT(J0jU++@TLiuthGtn%-#UaEO-F5%0*7yj=p}E@QzpC?keWa1` zN{=Yq_=+}a^VQF;<#nAPzz4L#=6&%fV8b2UZA>8kH|Ve)%g|Y)j$!=;)@kbVRwj7u zJ=LE1-FnEn+h*u_2V&Q1q@QR(Fl z1f%7t@l?1IUuU65?MvRcYU>}r00-QbQQI{(2taMFTJ`SOVxSqwf@7HUV&K5Ny*puZ z_QRi(Zp3Y7eP}x<|3_X2AcxVHVV7w&;M{%w`Qy*#*UWQ_n@I21u3P30%&K-9vTu-2 zDTr6h=VCyIz(8a6M{FL<#=b5e!e-nz_^*F5N4nx^I!NCJ^bUa&_M+^l-bL?r!!|4& zkz0WdQiiK7xdRa4{~#^=+_3yF?my6i-y55rQfk1P2m*Fox7@%LJCW9A-sypPErE7i zK-p~bI>_Lwwj-_8f}3wLF7>)@rABW}WX6Y{bf0}Oyw^M2SiSu3wR3t0asgH~coTjg zkS5@gb#lZm+AwvRk=D=xoQ|py`4oY1>>#kW)J5c)hX2s(}4G95bxFrb069+M44tg zohYwkE*>N+?5-x29k^fR)SsNVeo~ts_V{0}=3v3-6LS z(SzXp_T;n~OOoJYD#C@-MkkW{iRVTa)KinMTHNJRO1gL2k_ysNpc<56g1{e-3j+%f ztYw{#fiH#$9UlWDV9a$x1hwFmG2LX|<5wG96CkI)m%vj89eFLF1^v(K2}1V-rEF=-h;OPrr@3n2>6Ly2{Q|V&An50mq352?d{z;{O=SyRd;g_k*?0? zyd#bo!0)0>`NxUix3C@vbC=*G;y$iDl;vs^O#P?dG-6!d-mI}E^?0Tp%Zt-|jg=cI zeUe;Boexjs(p|W&V{Y};7(MmNRefWfu^WhO$9kUrT^Tn6i^NL6$92+ps`Kh2!P(Y%`Lim%GG8P9E)&mu9u@bXK!X$;VB{;{Fh zLoC%h!g8$t8jkHp7(`cJxs%c--jvjN^B0sp%1lL+lGX~4H@2f@>-jdDAcBp~)RaDKU{M9JwPNtNB&tH3-UEfKta>MXH@F;3`wRYo( zJGY_c&?aKVyB`rC+*wY=MKNat<9aFbrxh+uzM);&;!nRHu$zE{;9Jy70kfiPP4mU9 z$wFwzei*nXR(?B+VCK&9DpsvJDv3$FeMe-`GT5n_UXY~;d&75ok=jE(c~K`(B&<@^2xU>)bxR zkv=AEBjIU&lz(RV=r-TqZWZ1ykh|WrUUtpW-=6(nAKm>QcbWa)`ZnKh#x~y`PIBLQ z-~KfDzmL7Y{xR@-Uxc5v`sDo9h-lk4W-z0W7MRv9qvJ(!(Um|hIT1Jam_no6S=We- zE81DpNL&Fparo(DV)mYXx2NJ+wqdWOc*tk@JoJ;e2|zsd_K>JwK<_jC6EOO_bx-g| zf%~ZM$`z9npD%PvI8%#4e4y1XFkbq}-{fkL?XvGp57cfi zyhJ>ntGRodCZ@sx6;JNp9-Dm^Npf{8I95H?UfaU1Qp&M9L$@kqE&&x@rCSGtS7dcP zO(?qVuzva4^JiMCH=O3A|zd$NzfPD9vJecE<<{7pf<#@U&%7gAZu zos{K(1kg{!ew>(c*|>0h3&DDBFI)$*C($P9c8AzcL)&OYj{0e*abv04wP4*O-wI_={XKS@2lvC9 z^ak~vr$k%7-QAlp_C?^P!AFQQOYHLBoq|r~b|FV0$&h4&{`&lmTJ`2L-D`cAvx?3*%nK2;muBj1oWy_j33dl)Y$xdg{|fA0-$K6hno zJw$JDqZ9bE0}HfU6SX1h@Uq^M2^%XU>&M=6RT^zi#q-vDlnwE|Zt(UnmivBrFZ6od zB=q)l(4`+nEaZ2;>i_&_(;wKpCRPtulU?tuoF*!T#Kq_p-9|Sz--Mvv!>%fq9^Q%H zivcH(^)>IM{;!T1zVBwL2Uo0aS6xi$_J6P~gMHKE-unayyieW%$5)*Xg`!mJ4}6do z+P+xb8TB--!NHhD$r_5?3A?+DTM&x;Jmydn9guigrBRO%1XVDF?N-S+ueC(P_Fxi} zTl_}?`Jcns8r)9((>5xnp5FmOUnyQ9*uL%>yry|;nY}Rf&c2_eIi5z%n4?^ob$Dh7 zc!Rz4ZcS{SPrJyd)V}cGr%o~RcLlspAuH7^_gKZJrRyPVAE{R|kn{E$@28S}%OHup zD!B6D9ZY(RQ8sm!mG$RjY3k=#v~i8SvWbNi|MeRZF|nh5{652^bAcRgANRF+(El0i zZ3l8pgIg??#U8<|!w~Cr9r?jSS96X(FvBc}#k>JE471rRK#ssrJ|5lvxWnR3To-U;;+`IC@!8FC>Hgh`x~;2#*6K5v3Q zq=ra^pJQtM3BsedhQHBGHU3T~XrU3V{!I2ZA9P+jg6X72IFo_I+F_I5w9?And*_nH zhH>h;d#=|nN03DGhtz}MjsH7YjqRE-dzxf?&4nYR@$@ysZEl05mbYxRfGT`L*!%bt zQT)&>=*!Z91a#SSaE_{FSyt@tvc`33lt--zID4Jiwn7!lNJbBbqB=(9GAICB83Szj z(JFB~oF1xIb?Z->Mi^mKJjq(DVJvu_ayWS#{H*pRdEb^Hol+$>p_BT_prvC`n>-_Z zavf8i$h2*-EHB7B0y!ZBwS0#0bFNThRGK=3xhz^=Ieaw_{7mHe!f#{^5nOP~WPw%= zjsk?5+^QYUamXBqj$$crMy%iHIJY@?RC>NHK~W2-0^en3h_nN6Q9jd1B0h!q=SYx8 zSj7hx=|d?u1;S;@2RSKf@Y70hv4Bg9<~fEbrm$7P9XF+rcX$(E_l=KOrU; z&_cVmsDO_^nG1`{*wLr@5G6O31k$yC`yGBfEsUxy1|DjYAcASzqPDK+s=yyKbjlf` zq!yKAvjrxPso(*o!0^CCt?^v;v1 zmb($kEJx%95-0mY7daF|o9Z4F1@LG@qb>8|8QIDT7`aREE6yV&_&62kRKU%oxEw%B zqe?$1A64!z9)G6h{>VEgBFYBNOebbW zQRb>aOU9mRg#<^m< z8!Fm6<~~|&N2M`ieaJHJwmIJ4XwL(dvgU*yEb<{xhMDn*Y*_`U*d_R-=TZ`UOto{n zVAfLm_Q3i_d#(qp?Tvz5fe7I=;Z6ATeeq{j{fyY>np55nI2gef;{uiX~T2^?8UK*oBhZc&W9 zF@h9Z3Afk=z%5Wk=giHlCY=_NaPdGs9Jq4$1B)WP<4SXzNxh<0Tgc=#k;q^errNOE zZa!oYda%&mG4`3jw_!D2Cm9+L;!iY=e*TD2oF2{Tun?zwQKixl3??B!W9UbbsQ^b+ zq&YuA&?qt*mpU9o?R#A_CIr?Zk_&^#lP;_qjX-_No7KvgT(A!LY=d4QFO`TpUK_QL zHDC2uWtpcr{pF8RY;Zzr%VI(oh2mvso+gBKH@BBElYXDs7>EuN;zB(G$N&c0(9vURAe4Y_BCSf(e zC+;Y7MD7P4BC(NZrL>&n?isvcOK2rVB1b2c)*X7o<0d6lNYWbb-SHBfUW^LE3D7$6 zB6Zl7t|l2qRnoaSypl+NA_fc~W#o`J8GbRX)P!ei-75ZyDI~{mgk^xjrMmY<6KohpiVVey;%XC0ehD)zGFZaOkRS{7+ zVs4D&xJkC*#PEwbhKXq6xIq-gl7mY9HemIhvk-n?B>psNnn5#{lDQ6v{$Omu)B%2LLGyk5xm)0FTa*tj zs}!Hxz|BpeSSR&lSbb=f4Ldk1xM1j=PZoM9>l6TxRu9yHiNdh{&aIk+6cqR#crB8v ze6sJcYNBD1{k$MoPg?=vE)KkJI)j^UEy5NM3_Zc2%mg+*Q-jcqasF`zmOIW=f))YM zUM#cN-yiZ~k*yB5^LDYVi2R2RR8RAx49a$#t)6-{t4-ps;7yE@NwL1*nx5vz7|`rE z*Om2=7$XndgeD~WZC!~Fc@OVmUoU+o$~CDAXYeD9o~%29*E%&KNH&T<9u&b1Tl$QH zfjwyYQoNlOk_7$0cCdwugM|IX5G5~C)ny|sPu&Xs3grQ-dODIAWgL^83WVh){iRU;DZ`esd~}sK;pW3uCA~C$P7kYZDQp zSc-49KDbIvLq=O%bY9@93{F&#ljSq}EFc8Ea{BjoTL-uVD$NaZb79~<>j4ZJjCk{p z1WokM`sXn==D@|)A6wB@QeN-eB4FhFdbG=zFU-uy zx!{?nslMUj32oP5uxDrA!b8DZQZKRKZq^8#v&|!5z3h)q;{&pSR|5Z7QJq??O~$N0 zDZH(fc>xtQQ4J-b zFOS}w=D{8P%h%dSM*sW^zjj6rBSmGacJ*H=fPY8<|DP1#M=BaqtD-_GymwF)5E&ll z3~#1^-oT8@`u=e7JTm2ZW@bPxf@~Vz2;c#uXomh{*S$BkeeD~(IFOvsfu)6OHTa^~ zlVxgtYrwjo(uaC=hBbcu@=+sDUZ0KiFy9)sfGPyFG4skXtWaCTSnU?uu{B3}0TTsZ zAHA{JM!15+xy7g6@vS9icFn1(y%Jm!?X38*Us(+{*P&;XecA9k$+4ip(%h@j0ol58 zxnfn8cQ`1o4EnY`;Cb4HpE*6CLT#bl)}&u+2`w)p^AY4p%(7djC5I7%#1~R}{SNl0 zHO-2M-f6Q1olQG(BZ{b1<=09|wB30IjPI?UY>FXJKTIdF$eL_9%`hXC-x~@Eu`dI? zbi1L~$M1Y3Dx?X|xkN|d1F7uy!9Actpc+%omqkHm(zWucL;-%sVaMKV>(5 z@7La8)&DJ;znn=8FK#(CdT&FDJ7zT-r~KYtn2qi5MSyuLh8rNTnNZRCz3+>pC+XDb z@2o&;VM`L7qZ~$*$##emY)zV#Z!8vjoN-6yjTZ1(3XSRk4a3Im-$O;6xfQUYrt(tG zmg#DDM}a32p$p4!s4OL9#AaXv32>yxSD;P&w2g=RdTe=YKGDtD&3!2rYO6bmg@$DS zrqUH^+(5rw8%f!?^;>lkOWJxx8**lq7u4k5tvpl|{lDPH%e`QR)*0EF$xpzyY32fw z6cG8fj84i$#H7_}Et(?D&Eg&7{@Csle8_EG`o@M-?~0P8qg!LL&HB(ad7ZJd{sJH=5v5Q;K2{_y?Arjs@{a?7JSTi4g(a8;Q#U6GN9~W8Nvxfl zzl0R*g+jNCSm#i)+H~NDiDoux$2W*sw3E?eB=$a1Q)9G>&>z)@no5kx2}!R5V$l^4 zDS%DVU!HRN7^|8hs%smPj14BUJ5K2V^>AQDepVlf>I3_QnMI+$(ul1GC#^N(ei=+5 z`Ue&T<1}6kwp9RyQ>O%8Uaq~VU*i_2&i)hWajs)O08C1dIleLD=_9BZfdtD+oT_H4 zgXsjnQ>_uP#%KKd`cRGm*t}9Qlp33n1_RwIB)5G~Q&b+UUTe7ac<1lxhZ)T<#_+H+ zyV}X=e7Mo!LAbVb@n^lCx>UNC;RyzjXkrM(_IY3%I=c-{13H&Nb%U?0A$-0CSOl~# zi6H&jz2xlHISpuB3N$#u@sZ=Hxy-1(L?967gE9d389+LWS_kO>h5CvuN?n819Q7l3 zLo+X~krdbF1#(h2wgz(F5?*)U!w|X+wP0zT$Bi`{WxLf&_zq(L{@1|BAcwV7z!)#C zvz7OY?hcBi5iSQb;z!5^NI#Zj0i&=D>D?k&zk2`2ec}y+uuUMr3RpiDLK$QPBg$u; zfTf>H_yJ379#pzydgPu=COrwnLO<&FD?&II7ucj^s#+j}huaVfC?}}Ja{_k<6Mo5w z;LG#}{q~jz(WSbFg(SW5RJ9NQf_eG0ayZi>&PN z0Mjg5qmoK2eN!x=g%}h+A-|eXgXr1S%#ff2hPj7B$PERoz?G^;Wo+y(p1NU5y#hJk zr5$YLNQL4N6uC3nx5VeS{|tbVhzc>4@`6S<`U(#VgZ+V+LPw=An?kUSk$~Ue9Yhl| z9QdQyP5enVxt8v`X1vqa(K^%7jIAGN(x@0&6@KfqndOk9K7!{}xP`x_x)tNBl?Xth zoy&O!B+xk0q@gIS9Lv`VVOo#|&^|d7n~DlFzl`I zsiRlbX%7y&E`K6b!9XB@Wt%-z!QJBIVl`t79beMVtsrJcgNX^#s#Jh9y}h&!%c#Kf z$j0>mMaRPmk4NNwOKe6gAg!7y3^fb7Zr7z@QPrC>X+bxb9ZMO!iqky zjynDEpY_~*y3jp%kchsZrpdX&Crkp&JpxFSQq|dbM&cw^gtp7E zQ%sIfOCxD16t+oKjcEo_>x^Mcz@`O}RRSUm6^i26Ke<`>6gyXNU;jQ{rYW|Xey0>N(euohc^`oM;9RYzfMqbgsqQIp`G;^b`pIo3#)+xO@l;T zSvJx{%)1?YqiLfTI}V-7~v~am%-v z-wYUGkjdwm4&U}LX2~6q>iduV;?+^7rxhDRV`INza;ZvrZbjiRiGK@t9wW{9&Y0g9 zjzJXYoI&}u8mFu?$ZvzI51sx!nSmX{F!ufm$dHbCPPPbK)?}6B%9i9MOxNVsOdYQ8 zX^Cbz$Dn~`Inpdb%l40c)R$X6=Hizbpor;UJZ_+$TNCfC-vv^jD1!Q3MSYQQ{_L}G%V8sW@rD(8u6qo~ZiE7EDPW`kR z=k$^74lhl9kZ%s6B5n<6j^=4ZiY#)vDN(i=coYA)vMh2RFcS(!@&a_39~^n;l>*7S z=FJFEO3q>8i&JyHqB&;^xD#gT5NTR0pqMIClVj!{#=mlai{629h5M+{nFQ{Hg*t^J zCMwZ7Z<7l{u+qAyU z=N$aL3@~HTbHIgBjIJB~TbzV*iC=w>=No0M9~4(%{Q&xv5Ek13!|;U5 zt0W>Otu9k|=mgC#Z*7k2_`qXL%N)Hk)!UA<*GCPrGskr=q9rhhzAjDS(1+`91$G0LO z>ETjwoy@BP68$-c<=O9!snBE=O zm<^5o_>u)4?{!dY#pqO|IE^$@ePA;7u*Mv3YuI(B5G(8Mk^ z%`n-g+;iO6Mg?|lwE$1^`2CpJ(kK}?&r`r^d8zyyr=tov!N_SfOI`L;-S#oR zK~zQ2K;51OrsHIq0ZiwO!*mi{vs2vHZp222&QXy4DVaaQnkkutk5wLg6~pn9m_CEb z=w&F2IZ(!Ux|WQzL*v>zy5=_``tb|XQ`3Ergh^@n==2~!A=9j&j$k#!RFl@UjkB%( zkjWa05wwBX`3X-sC~vuefUIe*_j5b}4@{EAulHYb2Ldvt@e4?*NqWgM57ZgZfn7CC z4(5aeGS>9Bsmi(^$1O`4BPyAaQN&oK4Obga5&+WJ4D7Wjx}cQrGz9GMGpzN%wmNjB zl(ddGT!QB{W&KP|Cplt<6{pLjEw&gh;FwXr-U@WFTu0t%g{Op2iW#iO%+V|_G0g}x zrgU0+2|0)~o46|^IAAYy6^a;(xV+qN!8{R4W^mS>3wVb9E1i8CC&#Qw8`>l@7f~A2 z@5VQ)Q=7Rx=+=V3w!-|o(LfE2XSs}zJ2KT514RKX^I5`R3N)LR_jA>)qEp7FQyLOR z<@!TBB%qSb+FZk&GZ?>Cr^C~VIl@_ZWYn09Q17Rtf`T5Dr)n5!xt&oE$n9P}@T(FzhSxnum>}{o8UB zcqZ6p*?}91gff(fn1ue+RW&YeUBu@RGbkB?$io{Hy*_H#0J_y5$UJGw3H@r=$}bi3 zwwFBEGGoaI?6rtr{5m6IofS@<8u1Aoi0TVH#tSj?15H-VQ-iw+b)?^Cle7c1!f>$e zDV`O~fZuF6VCGA#Jr|8SBa{>LQj+b7oOhF^9 zbt5uFg@sR}X&PWe)~pITP$#mEt;!1yeL%+-Rmp#sfw91MmyepINk5p}9SM=k3wjVK zz=g}hqp+M1q9*3xvRn1D?oG6ZsPf1jts9gpfm+L-ZatVT8jwUdpg=SqgK@=A7U#yH z;$ZxkH8a9y`V9JIH>u~Vda6@x6~n01KKOtX(YtWZ7q99pE*s+x0(E7AB~jtQT(E1x zdx~v^KU;aarh+KCrk@05Bi&L9?^j@umUfjpbE9sC94sB7OYP!ZTc1^-{*VF!w@AmP zoeKiVbu84X$`yYOQ7!O*dLv*Dol7<5n&a}&yr&~WylcktiAs|+6ZpE^>@$K6_0z!> zqniyIfCIVdYpq83Bdmp+>^%-0j(mdp}Hl`fZO^L4!Bw1JbPBR!D=E%?8;W(;rRu6bI`9Sp7VEY0Q-ZW96$(U~% zXVT*#2>>q}gi<)I8}Al*1V(unN{oc36x3()6d8Ic*$NO~qI?8Rc*SJF%SDh$n@J8f zEmk^bC5Ab#HjRq-)|nT4G&=)4CdlCWf%NrX)db9M3P@ef9>wa?ek2|x!;M5i$)O+n zeG^8WVMh7qkm54Cx?poKpr6R1ZDQuABYIm|Q3h0>-R}xrC-rOzF7#)qMV(LAI%QM< zPl8G3L&7K2O%i450z3>jnlkO|)ApO#nxbD|U-+}qe)g_nNyh%M+{Ptu#7!HhZ5n8l zQia{A(Fakt&mfa$P-2!)2wXGO^~bl4QV^bxGCL@)SpXYFZd5TiuPfWL9C#g{6a+7a z1QGwrcK(f`QwlEG3srF)@_?rhGb>4VTFq*Wh7$``CH_3mN_+sY;^POhz^+={^m~Yo zQ#FLk{d1^IQqTYh!!*45`%9r(PSV=cuTgznldo5>W!k?_+x-r;g*^w{`qE6l!p==s zm=Xt0DHUHb_CM5$G)XE3ZFOPi-GI@H)7w!)`fj7|_AY8_>WY|$RA6E;WANxtOJi3W$ilnPWhJk7|DFj1-^Fpr?;joE*=_DPcQ(oU$NYxw# zikG2ih1h{Vi#Zx&8r&xioSRl6X}I(Y`ob+`gvK0n1Q`~kUZ^YmG*kLAv#7bK+cSny zx#Tj^8RGi_{3NhqW_3R}1&>ZNvn^k=QK~yA25~@>S@U%^&Lb!9@Bnty8}0`*9BO&~ zj(6;4E39%S{Ksd3drBVliM|lZ5)y$^z0a^n>lC3<9WW)>ICQ;=&~-;L24(5;OK?Ss zfpWxjTzY?hXH(1h|5y~U*gvR(yhCa@s~Zd&TR2H6jVpnc6^L?_$co1_tRtI6s&^oJ zyrK5_LrtOH;<`x^=@h2E?I0z7pD81b^N(b5iP8i|JB;B{z{=>jvRi6~yy5-R>I(Ad zI&1|s%k#+^TJY&iGcg!I4a@awpGt|Z#6)HlFm4WM6`8Q2CDnWssG3AZ@5M4|wy#o6 z8vun`BZ6S%g@wYF+piFzc8T2(k0i_iA2fP|R{_!PWDIsGe};^sezjgKG#K&RYIU=s zX}1+rFV6--tQ?v0r;ChM-|`)I#qwZJ%~+x zXwVBb1ZzX8_K=EJ`jnHVqgxWSCLYmuncZ*My8Ux%yfvBaK+fhu!0`rCy|QOsQI{r; zIiX|>M*fItVZ5AjQFj*pVzlb}961ps&XZD596`w%9acgGPj=I{sup5~3`j#KH>J;S zsFH*N5Teo3B3lnsw6Z(mEVnweQtiTAH)@lM#-(d?C5nLp+?d{eV|p=a3Z>U9Bh$_k z!OXi?e@ub>NJSoMov;N_PB&9)m(^ODQ*;ddiqo@LQ@Tcznm1Ddm9h#&C$z#A-9^A% zul$ENaA{hcqya}D#34VzI&r5+KXev;9OLR~X$D)+|3#riMYawsy4o5`K&VdZwvjn5 z>1NvLvz;0md#F8Y2CZ&11~topw#)9E(0xQWOsZdmVMpAQR|HDFvRaMv5)@s{0vQmA zf5`t0HG+^6ZnM`DyC*M`f=A?2rN-6`7`u2ozuQgm$F*(l#MFSDJtl~VuB)^LZW0|PD zEs^iv&kG|6eIZ2Q&zQeW%b{hFfL~+sbacz2*2EI}9y#yMNVF&b7nQ_hP34=yQlD#( z^1WO%-jvAeQ;~$q16EQ-?=MM5FlT)kEb*N=fqRTAf}Vs))MS6+KTLHnYQL}6XfU|a zK?LJE_68i}h$VU=7{6eX>Qe4}FXE}w*q*a5!8qGlg8mclYNh)`(-aA-uE;Jk`d!L^ z(&nA|v^;|@L{bp;vcD;P9HUx13Oya5-AIF0`qm=BCvhp_Xqs{@FM9H+7ve9_FB+DY zpPqY^%uv=(l4I#Jv3A>f5Z~+)cPRdq2?!){sq7jBr0;eIXN`r{3b-VWVehtaSxhjB z+z|Jjf6Vp3HVXetWsK@C_dlPWK=oPU*DCg)Ahl8|hf!k$y>3)0apM^?1T@R2Bh23% z;TV=&GPXx=P01-|%~d*h1!fei6Fw6V|CE1}jaz5~KDQ>cd(nO;WFF(3EP>ljm<-~Z zX7u|hZ=_ws6Zom-PZXKsajtiRGeoYbggWgBdX%;rTwX=7(8hJ!LP{Q-Y%d^BMm=U$ z^_~yc2K*dQ&C@6jh6Yj80aoOPUmUq2V!^87qk9jej#k}pQtNllr2X+?%$IbD$hlqK zX~gq7m~<#wNyB05PX&_kPZoq%5P`D_C=Z83iq&Y*iu1Y?%Ka;NijUQ%SE+)&lBO?_GALUIj6S$6)*tZ!@QA|MkMA7$1;Nbz4 zm^}|-06rf*2V#KUoNdR3Ql)3M1^gzYaIR+xk{9{s;=sy=%a~9LQSr!3gkR{@Xi<=o zWra+LBoMB3#eo}=WiAYX$a4@ZFT^?QWZ<5L1(6t_-e0(sL5g5salu?x8#Z1v#y)j8qoqT{tKvqU4DN}Y+ zvfX1-F(${N;m+}#8?t(;gg$=OiDa<_lAjO>+r#tokzI&mV$~@Az&9ZUt7=)ke1BVR zi+T$<7{jNTJSh%MSj0-ng!$^b_*Kr6E||Q^CFdovf#az-GE~O)ddHIN^DlagUJz#w z^xE$*W$UInbD}284Uw2XruMENF^?BW>hn5--rA~Gn%+{e)^ko!dc9kGrP?E~#PE4L4xy5d9_%7u%CEk4|BP ziAvzxiNWo0Kg8=FeL(PKloCY327BHWPKgD6#JKh&1g z*CO7ET$m+EcvH1cpUV*U)smHav^}LcmzCe8=o6X=Em}{DkAOHj0ADJO`P~ zVg|KBlWiKNl!LcKMztLiIf{77MM_wvM6Y&jw8@3WadEQLOYJ`KpZ%43lQds)CeF;t zsRR-w1<15yC1%Od$G#-Y!^}`p;j(dp8E!qvN@}i(b$tIH)qQ1D9Zk0;dT{4p2X_x{ zfxrO{?he6Sf)m``-GUt4J%Qlv67*mR?hrH(0tCX4_q%uQ%(v$LoB1)_YgJXtuCAx| zv#VBDSJkebRDoE2p~w0MEvYQgn<3V-e3h)+0tQ~o96T*wzBz& zbQCnZ`1GD4HFLp;aM&? zh`vu8UzOZ3mqlZk1W4PXdy{lI{EhOFPLgg>{YqJy5i#WL@|wh)2g=XI#u>jH)`$Rb z5gBMK^*cVwUvWbIxP?bLm71v5$e_Jw*LTo`7?J!@4s8c;mwb|tMv@TZ6_R?ACA*&N zGTkk+bh+#nHP|$H%hk1Jv#OnaB0~3_22GP47D|i<$)S86V8Se9v2IrO0VC2qW#9_C zBfRM2!dDqv^V}NBP+Saq!c`lZ^miI`#=kyE$)*xl8jGEne3OfkgeFIq%GLXDY~h*a zWl68%LjQNRzLKz8W3JHyoaWQYIBw{Dz+V`@5P0j}q+JfvQ9aWC z#pk8|iL%f|T7;#r4~<7nz#X9gbKw5wOj z5Ojypdr~F*c;FR?9qegAN7NpmTQhEWy#r4 zpI_51FklB^C?MqkHErxW=y{E*S4)MN7GitwdjikG8l-JYMPfGw?(OvJ6w<;T z$$Q(#NK)FcM`gB9N zoG5pl3-*_A1T@QHjaUikO$K$%(nTy02-M`XxG2iMKqP5SsrK-L*!S9EOx8r z7mJSGt}CDBGPj2NIZOvr7Dj)JB{`X?CD};yYItWvQci92932SXFfS&2F&!Q4%~9w# zo!SxGFeI<`z>v7JqOcJ!c8A-R>=aG5;QnB4WGTAj_E+DQ=}61;q}FCxKjox_-xtfy z+|}}=XTXQ#=<(t<>a__+bW&5^mC@tLr-MR{YZY-U0Q` zjT4)s=9|eX*t6vtrh-v@PHNR-T0((x*BSO9>q1s1w;9a)EQr}RsM2TSqm=f=PmCD|Jn8{z?Uo!7>^@Uxq zp-n`N^717U$_u_i5kQE4A^RfEU$WQOyf#e`9v0nD&NA;5HWYM;K6}AG)cG&`P3!Ik z_duhvA_;So^6ns)4Ag}4di;;dp&DJO0}-B5w4?@$PYAO5cr8i$4rm_9efOh^MeF2} zo!|<_4QmyQvxAmdr*zKuehQ%^L6x8#95GqMebJ6mxwbJU8s@~Lir0lKsV2JywtA(& zBn``0Bq+YL=QT~e%Zy6rr?FrI%U17h#*D&>cV5jVo6KMqYum^-wR)JOD-_d~S~|1+ zZE^GWva@w|K6HHB>iC9f;q;rU5PowP?-A<8-}N#xh?DX;xRmQVycMB zNjb|1g`75HZB7b!9kUI9&J%qc=2nDbxwuiglq&qFUStY{;aCW&ef1uXcC}vVH-QeM zM2)KC3ANh3s|8is<_~N1mT5?IsKZucg-&1%k02;y)s2K|czIxmmPRQxBea7Q>3x`t zGLW5wqcV3fw8J$AcE+fF@{G&{lEe|yMs)5{Dd0mRg>kSDOF(}Hm&%u149w~(RWVk{ zvhXO!b`bEzfVntfl2s%OZ!D{|coo>Z*BDM&{POY8M&$pg=cuW}ItIHlr`o7TiEC({ zQ9JlKoWmJK6ha+RF+Vj;`V!Uwk=}3qJc{Z3vY=?F^Dhf1p=a@t%b68J~hbiH-ETiYSU%78AzUUUZ+M@m4rL< z+{?ESp-JLKOeSBgrMfwtv@^-Iu0Xvd`5mQ;(BasI-Fc%A0RcJoKHLVPf;GeLbU!OM zRg0&mA{(VTc$AcEXxkMte#Jkt}L zZ1~b1gLdPp5Z?or3c)s|DksRvuk_iNM|c&P1$y4*2GlJBYN-w&hyDnr$2WV?Gk9ku zc7X6X0i7n}Wjb{!NQ*7AqLw9XBE8Bw81>ZLZE zUcOBF-$vU)OR3q0aE&~UP z-t%ZTpm`toSgqK7fTEWbcjjx6t!6!^5$)E$=yQnNY=y-3X1y~38@?Ho+GwOxb!Zir zBLtMQTJpvuBJ+IO47bdXa)p;da0$kN+fG)6y@+ttFb;b5!wDK@%kb~gDUxh#annl> zXJ%&sZ(t6VW47lmsDtjtV_Qp>RN&U~{e;;e8F?4wR99JVWM;##6Ao>0>%5NDGm`t)a(3>#I}meN?-}YN zVjz;r$~y4_5vESmn7toKB7T>mTOD~JXntX=16vrtZK`>SLJLCheVs+cUua;)0G4r& z>cf1)6tM@Pl&7m$OqdOq1-mFyLR>x76R0ymlI<4?H$&@0$3Q2JR8W=FlS*olk{lJ6 zqGs{IFkGwvtyDV}2V`UnP;*j+Ep5TWE$x_SQaAfupgDwY`NYR0kkC+7xWe!0%?UK z3raCmR#NReM=S>Ep;`O`whN9H`a&f08A&Nt>zihhGyCXUvS9^9f@~6*O3(nR*e(JC zqG!F-7@VVG`^?oEG9Sf{IDO471i~ryh{b`J*U~nl*Wt>8qpL$3Pvyg) z_Y23`@A#ShN<_{FGz54;Y(k19%hw-KP!LGd9AhHq_((KW^Ko)17b#o?EeKg=a|yu#SeV5I0yKX|_vPxe20;M9+y-1JBe_gI@K4qmscZiIy}J>QSQN z@FG59De!U%3PqoAL$UByxSekGpX5FmZ|UwfTpKRvGOMF`;l~HEl(f}BRf_Pu)U@&h zkyw~`&t-H<{rEf$#x6N52@w}*S>C?Og4u}BdutUiO*c;|(AGHpJobtpm!}}q4$5&9 zhv`WJj9!~d#yK{YLenJ#%KylecWPvKuHd0O#au1T#6l)+KDLlKiOo2f@J0lwEZ&9V zB*U-Rv7n1RFdfne7iG5}!-ADP@@o!EL{ zG93V+BT_;_HJ|z|=_k6MA}UeLhX)~{FPsjfaaf?arIyx-EIQ;F1j8%aaun%?%9)A?Lx{y3jMKCw0eod-HuZt>Q=a4y%ORiQ+EhTuG8<{ zF=Mn8rrds&GLG7O^noqEYyg{N7t?9NFa(OrV-8YMK|i=33JWs;)DrgMQitpZ1n+;B zrwDr&Ac&mByD`w`NROnvNzH$Re%z&NTGytMF48Cq2>i-6+nCfH=?7qr@Q9d?&}Fvu z12)CG?g10K8gVg4=#Kr4A{y8*gW`u{-m?>oEb!pa`Oc9KiNOqj+b(T}rD(<9&=eO)$rhurW>GGy1B%pxi_c!o@IfEWf_bOeioR zI4Cq$q9ygaR#sepFB~nimG~coo+r2o+eLaMVT|W^%SXT;%n0wu8ILb_&WE}@rVMR?iOCSg@%rmk32 zPu7po%2fT@3#ju)#E(J$`+AQ$@V~+cUlz`F5{8EZ{Wt49Dx0}*dU$)B8I7mzea7ov z*1efabW!3%bqP7J9+rFKP5x%3O%l1t(XP>{Q&(24xwdP+0B=Ck9DVYn z)WIauO35FG=O-Mhji;4oSkT>F&(r>PkbMQ^pZn_|amt{(r|$(pzb~)A%SoMHQ5V10 z1s#_!dcFs}+}-2%d$D!+Tf_5HgW2Ve=ZgZ$Xku@d+v~-cUqow0Lb`qLz>3J6L|r=l z_K>$gC!NztIOoVafw4}rB1}wcTidWO5ancuZQ_t8bB9G0xzd5HeN@_OCTHMj4(A6UTYJMxg|Hf9`A_uuCmd`NIzEBtD$qyKW+-Q$KuQ4D4db zaBtA_pSgUUbKc=PnQK>(Y2T$MOo2SSkG<)O^JV|IDa0ni9QKr}iXmv%ewtdDVPj03 z<@#__Np3e^(l2)_W`>@fe_t$177t3-Y*$~K!V>;&Z8?+85#a7z(Q54*C*vS`Oy#_( zbxNfB$*6~=;@!NaxT-$4W`Rh_lmpx`i^bIH%>L?(mt&ih$S}`*cCRNwqtBc>Z>$q? znl6Zm?iBmTYxhiN#VDr758O6g%CcfpSns^zSWcC-kaU>y+Q_<|2=;-7i*8<5v7<+J zav(^BY-%j>Www_>iqCIRK{M;UFK0I&K0tD7ds?!tTQxSl=GOe*ZutBC+DB+@82^w_ zJh|f*IN+4wCn&UJbrt#!S^%5#&rdmyGq*L{m{EP!XK#$h^cNCfk{}u#4JglskbLsC zzz%qac#;TX$ot63LBt!+#Noc{3_4@4C>Ul>M@_niH8Z|h##=P+F_3&)vocw7p;+wX zIQymiGXrL8_z}@%KC$iLu^u?|?MF8bhKI#=uA!&PJ^LO*#aw;U&)JHCLw^{pjT9T% zygf12AEIoOnVsYJ$_%?gQ+2tn-9m5MN#pqHZ5(k*8&d|u^cp$i9`S$cgl>*qlkRk6 zKDHiaBxy7@(bm4gq&$)0LTcTMD!9?p7hPIT{#j=E9)i{w;Be#2$@@oWTy+l3Zl+us zOEX4Pn_fJE#qU&5hs{~|dz0rqi&-w)hu%;kGD|A?L;l0h+MQn@&hBU53=jq#?*I<| zi;#=-qZS33#%UDdaE|;y#mZ%chnTaZf*&WjIFH0{8~B%XkAE76(4B?NC}F3J8Bcs_ zU_GnR^Q&I%m$K}1gktzG%V4W7?XI4rIPJh5sQJaEkDp6_%{qwVMmvPQN1@30ZooXfAqDCyY#Z`)X z5_puN#H%UQ`9~a%t>PiN*+Ti|53j2y z#|fF>{Vl-;TfA4|^6;Q0{Rf5A(Q#9x35R#(iC?5EBKP=@i}}hX!}(;C)X1MOX?M9YHbn8cLVlIVP`CU^@8sg4k6RzZNJ!^*8;ZHt zs;W)op^G=(6`Z_N_L~|n+b~M+-V;u{Io*wydyhnRJVT?%lQ5IVs-l9-bj)q1^%MU@ z^}TIsQ;uGM4ofa}+%J^?`^4-M759&9s-HeKF}-q6{gx6%I(}23O=aXv)-rc9tW9jF zO3vTE7X`d9aw2Kk*t-X|>L*fh*DO1~?eSjku=;fPDmTz;qsvZrb2BjD%Kw2okn&lm zr@#4d6WOWTQ}z2?#C!MhA2_YEwJDQvLl37nR>D#wd)5mCv=;Z51ZdK@fxi2}^X{VmHn^)*7e)Rapyh0cH@z5mCV4nr zKNa2C5?YVY!AxR(5Mc`%p|oZk|{JLn}tID>J3CA0pubk)z}na97#&Wn@h@a*o9 zeTDgUqo-CO;hu#@T$A?RM?4@?dibMdPJw2ehC^q+`)B{-#F;;+Ez{>d1q4Kpg`K>c z63c^ICk5CrTBne6nko0|54z!Bt6|1@+Yb2axx9)9wpNeqPlVm$RgeObW$6=x$jfRs z{dDi5gqEA8ObAb(14~P7(k6%Wr%Y#o*+ggAM(reEjQ9e`$!HbngotKdcQEuT96j)JW=?A`vI7g!+%kE zPo~meMI&}FYwt4So;M-PWnTo~H_WoJ?cklR0d@w0Fj#z3fy= zEh`lAM4TgLS?piaSI!hp>-RC@EA;A={GrNoD?{hb_|3xY?<~?;+XeC>nan{Zvw_ zq=7!j64?s z2TJN}vi~2x_W-;cE0;a#8`3L(q97uX*5K8%jn-cOr)y`bH*W|#*E^|x7d+rlsU}gv zq^=58Rk1gJb*?Wz-*1l&Ue}JUpHrQwK3(IKS9vCNHo!nX53aWD)Y?Kk5qAM)AT1}g zPVg>Z3Clomr4xJzKoLAz-#P-E;L+)onx@V>$6AvRF?|`8jZX*t6k{?Ft>vob=E+;> zh>F54L6qTOI=^b{le3_I7oLZ`RV}>W=H2%jKzH@kW$agBIGwF@hU_%Vd(+HgYbtY)@X$gKJy3OXKgt(sPDR!3T# zAAH69h3$KE47Ijd+&)P;VH4P^m_m7qI82reVV6}!uuOtBC3^nb8<)NBh{P@aUo}pk z1h;v?V`p+eq_Mey#>!pI5QkAT6eV1b5Yv;&W`f3_{s!vRE|Hw*PBdsNJ#WnMPR=je zN>+@ga53->IE3TA9p4?>Y4?vJ?<;)4r0O@6`;tlB;@32B>5AB%_zi09qABN*8P5!3 zZHhH>ApLAGU04!IJf49m@itBy({Qry{nR~Q7zT0ztsBV6!5z1TqytMz*MU3COnRwK zekQysrG}&nD{T4d#{#BPJhh#)vvNxdu!f|Ee^7Tax~L>{LH-Zspn`OY`J+y}$>@VB z?~vp?p96Ah3;q)Gi1YB>qPYzaQC)i1VghNZ(sG|%FSB;&ja`QvuB5lqgKUeGB<6_AQ z!|0zdgC^pJS?22;)r=i>M(B<|Q_fSWfuiR!!!%3RiDsFQ*_EaGPWb5L8>^w1$PP>u z%C2)wnY<6tpZiHw;3r*)pv}0Bji>_xviL-e5glW9%D-RxR z#^zfM*}|#a-pP(sm@V2pk~`nY2GHHkezc(=8s)no^SeRvBhqnILfrq%{DZ?MXOs`a zfn%y^NS%BkxCA*dQz+Jg%;jCMg%r7uQq8swRwrnOInj6>A_)1+PC!;qF)1+pC6i@pL~&F6WBntPV`D6 zWLQ0dy)Jx9P4HK3iL;&%yI)vJozA9T0}vPLAO0R(gH|r*BB?4RFlDclD-LiyU}5II zfiR!)%0n7)b?^h&pJIVrQe*VOWapYh-0ZTWGVCUC^F0*p(g#)EFA%K7{VyA*t&N;e*7K)x~ABaK+ zjrAVbCL3@VkmsW*-?8Bem1p06H{M_xo+>AaexZbQ#+2xiK5VqWT8xT&zm9RSHYS?H z*L#mmWqZdRTUT-ysOcWvfXaEvfw;+aiG{F9`~N!i1terB<*h6f!#Je%xCh~iVaVgS zAC6PQmzU$dOSG{ScMUHgD>&t=qm6}2*=&P`J&u6}@UhnLu<`<-`(#210F^jEy}ROT z_Omvv$r`VXnIeCx;YIf^G!Ma0!H6zDPdh$LpET@a6xWyr;qcN#LEjys0>QdiM=IxI z@2AswTN256sGze42^BTr{82QY2yH^-vL7~0)&3KSb0P4RFYqFPIp3Rgr;2*~BNhG2 z3ug87O5VU#g@^*_7rSg|U(S5|y*rGzO`n6Sc|{v-imxd2_J?`Y3j|Li+b7JwuUxBy zd#O2=89{pE_tv_iBnzJ@Iz_f5M)_>mmDH6>0?T z{W^P9vWdw~3EOi9RIiUWqVO6D7g%>aoj3>i{NfT}Q8r?H^UC+3B{B=LX~>=<6e9L& zL8`5r{r9=~YTWTN%FBEi1VXs~-tpE;kK#XDbI9N5e|5w4?~?!Sc;;VnaB$5bA}@}- z{u?(m|DMUe_h$ZACak*u=S=>iZ&OVX8SwXJ#Fu^SCH8WFgTsZBakH|L0!vf>XSx3e DY19Qt literal 41012 zcmeFZbx@Uy-Zo4~Ntc9_q;!{bcXxNEbcciz(%s!1(xrfOw}f<=i7Lm!p6h+w0Z3ND(|Uq(*~>y;!-z7QzYV zZt&?g9(U%Cf$JB=_`~7S4~jJSkdd-2|QAa_W^YNs^M0_1kY~#ff6hjxuplGFyBM)Jsg2ca-U*JutMW zpqzead_HN)l$kSb*YcSpQRH|x#EoGA5| zh;pJcl2U#mZz2@!FRv&N`6Pzq+D_KI9LXHM&(Uuc9T!tXe@pJ)@9k?bUd5`vaI(?) zhAM~0Q3FYH-$LQeXmrR_^}xZk#2uQx*yIO#UU%`}#CdT6Cx3fk_aX4p*MayG0}~uB z%z=I3CacN0T94@Kp#^UD#Rbo;)|4)11?P&r2Aek($%xH8ji)> zII3}@m`w7;rwdyb?4etx#XS<~)NAjs;x9tSpm?$_Enz;5^u}pojMHI)cWqNI>=gZmqb_B>Z7bZ8U*2QY29gCB%ZQe*6Go~(gml{)~ zO3_UkD+7je9gOTwchjA_ZSy-{Oc^8`7&5{*^dIo%PxdFyS8~}KQXmcN_ULM}dAvd% zuDOH^@~&MlV^^iW=mc6~9l~XUbK~_%9z;aIT=aAz%yizGxHDZ*HvDj*e<%^j-sw44 z(Dc?M)T|FL6QM~gSyk<$D+;6)adCF#Iy@n2^7qSOJS0iKXqDNo>ARsZD)V>HX2YS2 zR-?}DHjdtCg^`kklM^^eiX{ypO22-{l8!;6Fm4fSbl~@7X?e3p#m$OgQjN-g^41B0 zry4>ozVyAD&3?q9OEpP%mt9dg-o!~TLUwwhm^|w(&Z$C?(L9O9!=+8j+lgW;47v?& z3l^#_42lAHR?UPz2&CDkgfUYPM4NXiQI-&dmS9)%HL?#zgd=GTbUSR%8&vUU-ysZB zVMxG^z^aD)Q9#R9$Ctvn=62P#O(KRCVdpY`)yFfK8uQ}o~4}7Ri zKE9Cu|{D?Mi%+es8g7l zHnKQESekHmqmXq7FxGXd1q}T0n|`stm(WKRCM!t2(wh&j zqD-CDYXYZZO8vQXY`vzj9=&}5b89ZL#mPpudNcJr-ZokPG_im>T+pgTjclH=;g$u) z6qBdNaeJxh{^Dh2G8s&r+yFJ#pc|^jLF3z0rh;JVPnw_OH29imXuDP^;H#}`)Jt;a zGDF+?n~gpcv5S`C=Qv&zI=?ypl4FeCT{e`-flsR8u2L`4Cb1waVR!nGJxd<-KBi~z zG;Foan&&=zE^M_sZ1o)X>&t2=eAckGS+DDS+!LPth==Xz(q%@3@qIrou`y?EXOeU# zcV=(*>4*x{o?ONK&BEB|u7Y4{?) zS+{ZB)$q;7&Aq7#Mw}Y#JG<%)hrT>|)A*ZGuaq9>#Yl|;g4yE z4YXDV1IeCsc{*dYuElO%4q>Sn>@l}XxOBuuzK7wNhW@;-WD9!G)Uk1q#^Cvjo47L~ zVxU@i0+a=w!GQ`0b<3_e*ov|u1j)ep2+K>$Dxj7kq9r^dMNbrax5P)f&{K1~hBZQL z1R)fFGV_dO`!nh2kx#U>ElRo`WO7bQMx$jBxD>BK<|y$GT*I-Ck}um}!I{L=_*`e7 zvG_`L8AqGCw2QH_1+^)`u!ynqInGXiB>Cj=XNw(i^yIuuWbxv_H;t-#cTxpo36>l@ z_{&dk+k~);vU|5v)%)xZkGImo{pI7XC{K?U!Z3kK1qerfca9KaWw7$_qX)kBbt6>3 zYe!F(%31~`Tlh}wg~2^Ty#8#`C>r7qkM9+N{ncx4viFq&yfs=ZwLYX7g(IdYw+IZ| z!g(LH@O(+hDPPkjx06;B&RC+rBeHEnlD%!ytq|*jDeBK-8F|KpjxF``eXKm>{EOGp zSuXuXH#!AXFTT9gO{G)=`8S- zvXhq8WWh?ze*yRxeGHQ3{iNBDfLBE7Wd+96s}6XRf$u$p$!Ld8 zLnQ@&B<(mPh_*IFnIh$l&Owifm(oR@QM6o9G~XQe7BT3WhkgIzO>%1{lF8 z2VZeQUy<*TG@M``nevdI6RW)}=tSNjIud<}m&qya3iCkAtKPaZ7wPHcpuQiLYR36t zQ{(|u_7BJHx|n(YHW&y94-p6m4&b;|F|#ugbTn|#;n%Y_lC{)xFrrZ~)w4CC(YLc* zSJkqaqeJt&EI(RhiR+zqLUAd{aG$FY%Q#@q!WH7rS;>}vxJ!A-D_GfqFD1#$i>AiP zx|wG0K}WLMwEe@{PR3+iqJaX#<_xrTOy+d>%%t)gT5rw_dU^kTm1Z>ldh<*nqQ-MU zuC~ezKZbJjG3^rzDa&9>_2^@-)9X(Bx)=$teXs?g#;L$YYjwMR`2}?_Jvyt*m+GV^ z7h7;xTnwqWl0&objS7f6**_Q)#%OTnUgp-B+#tR0UP!^sfMfQMF%0bJv~hKeTP|`3 zQ{N=x^tXM#opMlazzU-{0P>x5(*$<_U+rP$gYvxB@>;XGU;-nGlQZ@HEA&^14?~1+(&mDARVQ_7!OjVFz`j*m z>>uL8Se@sKn<+uoTnkw7+N`Sh-UpLeU=o_?mgQq2Wj#=5AqiYtnR0=cMi?VOTYT`tPz_QM9 zK$i`4)Ph*A&(k)#vW2Y{Y9r-)>zAySpM;k zb0!Cfphi2z8oU^tiC9+0b>$MD@fh7Xw?MZ8^1Y6I#)umEEcFD5L_8A|iAea#YI5KSRW?xcuuT03vb#h`jtgL?n%j9aQuzE&nY-F6UsD zE8iWMjg$H|Zhmk=gNY0d7r(n?WcB$_5rm71QX?;iwxF5rK6D&J{*pWNJz};9ckj}P z5ckb@nWI?mB(Dvg>OqPS;aO7m^zzNCsJov0Sz&Kgd)@1EsTlA20>^+J-yyc^1h2%MM-EW>|WgTg?V6FB5QtFc!>&9m($D z^3KYz_S5$LaOMsUEJ2JimTiQLktE&vcB|obHjtrDyAiQE4cN-hKkf{p>~rcmcXFss zHEZgPd}V(8s`txfTUbW5&d%sps0lOF8ScLKMmk_ODxbVPjBiVLrsa1TzBlpA!!Vz$ zkZzh!(0<+F(#6)K@KlizzdPs}bfsdq)!_?s1!0n+q@KqNL5~mXA~E(!kWOX;5@D>rZNe)}OTq8hbPp<@2#v@C5LV`BujDK6q6LmSZo&%%37~7%?ICZ7=jS9|f;mJozr|&!n{|p|bd#I^uLW&jiHjGM@SM!^Fj)z@cEgfi&*< zTq3h{AzL@0A)uuQO(CP-vI9rjOaOl?JaX3WMpO`eDlDcVn?o6u)s^8Qr$V(GEM4L= z9i!`Brm<6*SK(gAv9EZ9D()7RvGZI5hq!}aZzxEvQVHs-{d+AHr7>n^V%~J@;MK)L z-^Lkw8b?XEe8P9CKByw#b1(bkzvv$y?~->WG=g z!t+N8VO2+bwFSTj>z^q^-ps`GZvk?o%6C~~7G3Z!0O?@0hBnR3dfmw0dYQ6{ia}rh z68<3Y@+8xR+PG^?i~F<6mD#P@{eVgcC)P&styN>n3;W}2%uUK0J#xO8X;!7X$f7Cq zw;qLfHPGbGeP8&Uuk2%`$Dw@mSp}PxqA``xVvHa~8+WAkt>+eSzvLx#adcRuONN&W z+reJgKG+ey#gf{VVs!KT6mDnQ)Ku+DLj>ykL4;q?Sn|YOt*`{^=?M0i`zD6APlv}0*34TRugD!Z-iH_YJB=irhfkm<|O~l+e z&wFNXRmsyUL#}W4O~sw2EkBaO_s0n>zA@m_^g~>VPiTL{YCe~mQ$MxBhr5UEEibx^ z8|G)lwiXGZ#S^BzDwguF>Rdg&ZxWhWAk>6>JF?aTp2_m##q+)LA1F}XFYfNedWc-i z96uQmuzKU@_`XRJR$D~V{CI(`+G3TkG1a{|IXc&BU(itzpgLcFBQjkvC-AX)Mif_7 z^A+R=OjH?3D%zywcQNaUl1b`mugoiCCQ{56{dXtUKg?BUp9tlqIa@z4E(BF1nyt19 zQYMv%9BLI^r)e~#prX(r*+Mb4$s|dY!9u9?RVe8grz9wxN5PUe$ew<%A*`>m{RUQ1 zRH^Ks*T5si#8}zsz|JzlIi27(t7HexH#=$YI=0GT&^#BzvBlnlq$Oeumb}Gh3|2Z{ zMOlothkt9UNkiG%v$1uvA%F4Z)=p@~HNn_$YAafCF1O7yiMBGozE-so-HvhD1nRGK>x%k@J?XnBWko=@x`SHLR(p0 zmc?)+h!&XQP0JFfJqmp(fpHira;+*${srV2_|9|Ph96t&6s>mfeI0y&<03n2bW)J}zD}4n^8wVXa`ri|Yl+7G3Qp;ia zjDd!E4P(>&8{vpF^;(qI>>9)*qR=89f_-g=Zg9~37BlDiDFse~5zJ|+u7`uC*es37 zp$FZU9!YH*{T}y2_}@7m7;gH!jlMa)2(7ld_%uUxf_{@YTvd8swcLKjjyCBRqO78N zC*L$NK-xpf{BnQFX81?VaslQ9xFl<$JZq<8w0xFP$+kL(m*d>jVgy6`tDIQy@wDZ$ z?Pe1ujxEkxpE)?5au%-TJ^o}?{?pao1&QQ4X(K2lx8OTvUAI^<8RwSwe5stG*?L9N zh?4qTr~1&bO}wvU%9fJ*Px{z~(yGi~gRJPO2MI-ZUZy*Yai2-F-JRf$BHoQ1LV1hq z)M+m{6@GudOPQ{X3tO@A5Vz|L(w^>d#XYQIH_)|wb2#bH(xZI3VE0f&Ib&FNpFDVm zO0=WFFukbqj5P(lT5o=cs0WDkX)mgaf%+xztLR3J!IN2slc*N+{`g=n zPJC}w&UfuQ&m&0_-~!tPBX#n@?!LBO<;u_)wR!6|$EEpk2jn&VF!^y;egEQXL_DjS zNH#Zh_9cYElKgfPr2LKyhUre6q3!P5jaz~Zdb06Ed(ZkZVV4yYf>Zv z1#~JC>f1?i#%J((&jtflV!320VGDz-CsP%Rt}-sZcKP~e<@0;`qD`prr_&4F^)P95 zBaqX}u7C<}I)E*x$7{LiUx5UZ3bST7JK~Hz=^UZmZ}AskFO)dOc9_j`ajfbpu!RY(7u2t`L>{&aQlc4s zj%d4uge^RX^RcvN-r}1%O-BIUwqmonh;f6?Gw{TKgLihs=x&091fKzbThT+W7~lTAM|5_Y5WAsx>E(wfiJ09)T40<3o)1 z(m5s)Wx3vT$lI(rWWK2%`vdMa-4;Dt)(nj=m&VPG)u}?RI6Qq0&-(W$?_zDHU73dJ zuDYuL8a`hzN zIT=BzK_=ZP8$#v=0a2xg&Dj^FM=Fh&C;xm~7VAiyqNq$K>FdttxdGq2s`6onno6%C z+6#@Aog9}fCAZBF|D!`ZDbj{K4=0v=`<@ ziSvlTk7$HXa}{Lcy%P6D@3}D6mal#=65?3Da`ZlWBavu?8By;)Et>JrQ%B(72y=TM zG=lvG!n0d__auG2Zs_{uOTC)LUiPHzaeB9Oy12oBR*UwA`!n4tP*`v_8!rbhY_$zi@F9wBSU`j`q>_gnCWWCN@Mg+s2chk1B4<-!XTk zAZmMS;t0OBpcuqB#-vpEk*GfruGw!?isTFG3q{3lg1Y5k$)5Q>N!D$KB0Fd}BIMg3 zb)bF7Tb`As1dX63E70aFwc_5cqIYm*29m03ilSN0YPcHB68cZ&Ponj_%%;)ZbE2<^ z%f&XWqA{q}!Df|PKt3|4n^$%M1C*r>oAzq4W*PvF< zQM?lx7;~1JjGBFGGg^{MSECpwErU$QeenaNQ%%r}fHW=@F;A5Q17B(yt_Qx|{J6K@ zEEq+Om^64mcgUsoT^gD{Z$Lqvw2RnFmHr#VKLShFJ!kD7uoc7qa<=-cM<|035Y!s{ zH*D3q74LPk&XJ`9U@iPx+`=U{&u_W!C`-yc+#IRelO8n6mOw$gPu6cZb$aPB-XFH9 zjGq%~3_ADU6_@EoFI?NOjLU=;*z{_;>mbkDDmNQX<{vMhDx?-&}am{KrFmjM$DRmdy#`5FGrMm%>z%-wy^vAP?= zHCocaO8WBU@U;exfcByx?#Jtz&_xVMPl?<^Y|%mw_jt6Z`HSUew^wBvWDfba!WTWmk!}d1GzTsaA za2C4ggRl+mRz3(=|3Kkw>S2W11kKmDEnItAj)GHhQr_uXG!isZ5bGy52{ovK1J~OT zfdkjm@x9qk2p!6=2Qq9z)y~wA%sRo`-XS{k{bpedS)iBMAqh^Om{#y~Mn6keaWu7X zfP`!(2}+1jh)bMAiCvJgi;uwj5jyh?Q%j)@?QhEVFf?bP`725s4E=Y6m_1Yu7%@bN zc$XN1nnlm!gY?D>#9xOoa)UUs7^wDQVma|B>Z_%&1(&gBmJm~6q&KI;VamrvJSQ!smUsE56A)~BYV)74)gMt6K1YYta-;HNCJxf|Hj?ctx}{91 zI!ujw*bH@ue@}3D?8VY_A_!qHxRYd088%Xe=?UlQVsa2T)`FC<^Q!xsL7h`0Ztr}= zqg1a%&)l}E{Wl@qvn=i!mM^Y?ZwE|fr*3Ct2{xDwC;h6FO+@b$IZRhj*1)8Mo6f&6 zde6zyz)XN!V$5c|Txs-0o2Q?0qIp-rsbz2D6Z!*btgmA{Z5ytz*@T6oK+}`grf7I~ zkoQf~(ZOL;2-};TzXp?ZFD5C5QhY?Ei%%=KxX8guE6G z{PS}czh@g#tq_^J65lUn{HI0=N5}m^savTY9H-J}p!3&-hDPU`ak4LA3}*S0iXr(E zWDt$b$3;1gynM0UuWR2AWGRofhcc_mFJo#hLquR;4M-Fi_`ipWi8s)v&|rs5q#wS9 zV8n=uKVTr?PBK?0YRgMF?br$*Cm+1n_u4G|;g{`jsh^RHB0ln$li+0MSx+Kl!JsGS4$pSMJ{Fl%?(>+er z#cPl$#_<=j;;!4+bxGO@=N-qf+g)$n-lk~#jKdDel-Dz~5IAKL)#jCy>=46qS%q6; zZ|pS3tQaQf%sJABJIJT6!Ea|totzjt%Z;KCoA=6fRS9}2OK49^-ZmGo6xGu9*O^6;cGQy9dY#RaOx}_aj@8 zehtgBfJ8%Yw|iu(=W=LIZ1u@MZOxxI$n=x6OCRQR&!5=}>t&AbOTZcUH`5?9j^GAffhU%cF$Ax0-Uh`0wbh5%>5APhHo5%|1Qwhe_CtxQ z%!UrESPLHyw`*Le>d zhTMg#{e~u?Ci1SE=ivOSVXYRS2gAuO39K$SVsRC*USDYVVkwexFP!5A?ykuZzK7xA zk|#MIsD6d%?7PO);$_3#FR{x8)mM}WA{v;8+|PHm;5{dF^LC2z4zJ%?psmFxc@!TS zAg`RYmmCYXp6~WYXmi2Z{vxmRuefQI^(AM2$}5#edDY+ZbP1vo9X!e_LDrPm3^n+F zkyrgL-+#)hUVyx6Gw*-I7EoUC?mX|cq*H_Qtzc-AhPV#*fR#ws?(=#OG^OT(@MMs3 zz8XHcF}b4-vsGVM!x=o^#%!U*ym=wN#Eiu6ea)*-M+~(_t7}l+cCn$xLW!MG2`NE= z;|H$`;v2c#?r$#aQo!qKMw(X@Ym9`wU2+5?!-uzWHjbxjX}+FiGAQDG?lavpa41Q2etSkk!0M2@e&^_A0_69S7 zkf)!7wP8)##YY6lSr?{@!2>OyQa4aMwve^E`kf4wZ-go$)OKi!gc&#vXpJI+5)SbxCQ?|qAZtgxC{hyRZ$ENc_dbuQ;>zrJ+Nt^epnLSi=iZIHUv6&pmU^?C|yQGu!6Dh>Z+dGq$mlb{A=c6Jo=Xf{9sf;nD zhkzFA>6w@P@mwMHLbGKj>j&{}wA)S`Mk(j`Vg-ef%IQQWj-v{3# z3m!PYrMX9cEB3L}_>s1BYG>{F@Y2G0h(0gBke?P&K9o162Syup!cJ>n31oX1u(` zFs}9_aOg>73G0B|5Wc%XDZ)I+1l`U}@DElF?ikwA?;p|>)vh0pxwb8GD7;V6AA&07 zp+O*yVUAr1aS4qjzm=rl5h>fQQvT5H(+(b&5-B#gSZ=Oiy_HEAgICpOx?*7WmSahO zOC$7#xfOOveBKE5JNlqHJ4W$_!ZT-5MW(`VxcYQ+L5{;BH*o32T+W0j?Pk)_dDv?1 z*Ne8afd^e9Eg%c9Lrp6B^%%r7`#mM(5|kWfUnIX}Cl6?*_tj%L^oT% zv59VGo7KY9e8)?JRizr+Gi28*Nt39+)1J+5iplKGWC6SgrR`z3&uq)rgg6+AO9@qb zs2nkrh!SxxUkqwK_meP6JVOIrT@6L#O%aNL)YqTA(hXL!JC<`8^qB`Kc>y>G^Ijth z>1WMg4Pcckk3_#iDnL4)f}u5AHh24Ts+1c9c7^r#yv6jtoVTa~=~M8c$lNw&kwTl! zjFPn5Sku>Y9aF%KCS^g0zYKg>C%8V~+0XKfy%2Bvda^KLL;KzM4C?bzm0_@ytG4xPT#%zj2O}0!!Rhzl+F@XrEq&NvyzBgM6E{$Mi9QN%LSm{cfPLsG z>Q32uIN0IMcU6lYa>s!8gZDn?K)Kq3A+>p}eC{NG7;*S3{)=z|q!9l9Q?!-#3ZS#% z4>(De=F=^@IovQSQ-r&<-&@A0;m+pog&bmWmv0Hy_LTEM0@Ar zDU#FbuPP%1D&aHPuwjZot#0Z1AV6;7QnBE`>0_`lSoHSwNsrM@UTl#HsB9LZJu0m( z&c0p`fYJ(K>FSTB4(uQiU5g?LQ3|=Gil1ML;#da7!{z@{0K5koF6W$*B#tsgT+nI1 zMXK#%bnqJ&HWgs^?$x)MZpdS6=EaH0(7oaq`T?>w8`{@{so}(6aC;vCB zwlT~Jr%&A9W$ZRz7dYP_=+`Ewm@f5n};R>n(K%~rpY2ekf6*UA+GZ2Z;- zZK>%H59TH6%knyUpx8Q1^b#lL>VFbj1}IEGvBmX^*dn-{L;T`(h2Re7EpJeq$dxFq zeWGG@p&9Xhb9MNGZGC|1ncV+5(sI750*Eb)1EyT=6bh{(p8SMUK7iN)hcS}M{XvLl zVDy|vA@n-ZqOk;>kdENUhs4BCzV6auF7BmU0d(&B5{9H737Ydmb(q&LR=qwRey&4t zo&}tQ)yhL%MOSDYsW9kLJ4E0I`PidFO+|}7(az+Ec{v#6Vw9JkOAv!u=R-2x=^bc! zFW;RW#9|&m1W9l(SoDqcNzc)Z1Hi=?zgf6OX8fBqm^}22m{!^e12UcH00O-2o4XQ^rOn(~Je22(2Pw6@uHeg;1{enSaL16@>kaK+#RGqjJ^kX$W(6kr{SV4-r$Y}G)#RVf1UOPeBcNe3d0Zu{+!4}8Q z66*Cyi{5{{2#Gu68~%Yc$ozZKlCoKOGBN6#EM<)?H&=myZ?P84L>kdtuIL9v55=7} z9TTt*uMuYGUzNU~2rlV$*=S*928`N3 z$^!Jqq?v}hKp)8a2xaYr3%GolO|I-&zRewQa|KUrE4R zx-GWZx10;MY3{MS9!hw+=-1YTrbY{v;zn=Y723QyR|0=^mJiG1C{e|G55}@_Z6T8; zRGkHiZBViHnV}vG4vB6IA5&)#8Vdb2*`T22r!hD_KXN|%Oc%3Zg*|@rEDcApSaa@z zySHx%GPeq4bqM%(s4|BWvb+qFXbj%RV7@*E$!2_0=zgnI+RxBZ4j1cJA&=L9sWmgy z6wwSDKy;?%qGDLC36gJWxvs4mR&e&dTZN@E1j#?y+K}Ucq63ujK^&(*Ub<7b6eqNZ z+q88>R}>*|O_%qPu!`6)1zBu!x}#zvQ|tAL21lg=%Z3!lrQz`NbN&TY>o;p+Ku0+Y^*@A&nVd2 zWyG~ANiV9|+)={l3aOFVSx`9)blbgV&E_t-T-aK4b)AmQI$9SJ&Gp*lu=oD5&hh#| zrqUxFgOWS%c=_@%9DZ~!TwX@^THmpzZ#M~!Rcn^D#D1`5CFN7VmUmt{oz*ztI)u81 z`dseGxEFs|@$HC=`BTzt^!6Hw_oVSKgsThp{YU8c5+8){&uNTaFcQ3lDUalB>-|HF z)tR(=(to03au@WGPF)M&UYspsw#Z=iZ}96df7kSObI98e?#n#HP&bfiyNFPm>U;Ud z0RBY`2jojn(7h1o1Bn9t`uC7I@%ktf>g13d+3Dy1y~J8rEI0fAi^LlKO=8LXmBd0+ z?$*Wrdx^#JN4EN{cCnVI<&8@u3bfo~QmCU)i2aGU3D=jLxgb0H{%!3-z4=AI{%^I5 z^>2=Np{G{&fZ9bc`i@39trf`XyG`+<#hwFG7AwEMy>K0VURNj5S@S1z@b}Um5Fn66+VndJ2gr zpBQU+;umZ1-!(4)jP<+bMH>ac8vKQ^cAg_YHZMLiw24FPgnOPoS%aM})Lc-Gfx!nS zzh+qrfGlh0Da(Q_Jivy| zbP{ZrgnJqahP=XL5l5loDc}C%87Px`4TdalLheZl$g)&^&9c%*#RgZ)EwtjcJ&iEg zp($4@Lq-Fh(kwP$n$`7dnl<@B9GGTB*F*EwZwr4qEq@M7vnG6tdV+w?ATZ6c0P>Y6 zU{s}RyV#*W*g^2gmxwVA2WwUgJ8uqW!EkHz7iTa4Vcn3Xi*iILd1bx#g}^Eb@!OzI zfHRo3j^g;&-HT<#W=j#{T447g_u!p5arI;OqVR1ZHYr!8|1zL^K`?9_S`%8ScPT)k z+!X-qUZAOeeoTmikd1;0WcdTl^NN*r#5yn)u_iBqXvmi-{>8lT4mR_`{RbNB-$U!4 zb}#<#b_T%$HB)i06yEXw4Q-v~{Fk)#)M^PO}{B%Qrwp$ek{b<-G}Ct_vBzu3hMHH2bCD=$uT}usikH; zlSLLsR5M~arf;ZEwV^+w?!Lob3a}wLsDd~OHZDyGQ931m$ALu1BlH9O8R+Nlz1n@shHf@)Ed3K{ZT1nnqh@r znStA}GRg~4vQsf_*3>#N?UX$lI2t~JcsR8ZaYK^^L(=$xLj3P?ub4uUxRPoK?aQt5 z8=m32)vKIj;U-QhBU2g1HHH*L4urXf7}ErFnJs{N%5V^y%0=v~S29-!EiHeE5g07$i3H^*<8bv90&??INX z;%8OvDZiCf>!w&$*L4|3md^}(#95uk!$ai_>#1_N%;%?Z%RjFPW!8diFPHu{_fj5`&A-M@caAZIJM}Wu7%H_Gu|VqFwRRf@ zXuBgP0q#NV$9u3~OkuAMY`j>~t#OwNZfDIG`aNbkPEatQFSv|E;rvr-t^ZDHp*#{7 zKx!$rj2%{Zu%0Eu)??kJ9XaQC?Vel3KQF6jJ=>nF1hKfcY4Cyqx$TWVJoO;y=H&Ev z_%$!qaTH(yc!S13Z}9rj8|;8L>9SDv{8w-A;OeJ0$dcM@2=oRGe|m!zjr@eDl8^$r zdVf_J6p(U09zJ@^v|iELRD}t9stX^&(%i&dm-!-Hk3%FilBf}QgTA!Ct7RR|QX2*N zNUv(AhouP$35~gp7+P-Z@Ao_=+L(|=!z?hPBPOFkwz+1L-02nbXlY}#I5YmleLY)* z?3rSL45N4|$r+ZyM7ZR*(Ov`!I0>GvgD@F$cw3pHTHa-BrrlEu1NNIMMwD}=?V~ri z2k-{bJ_1bje&pEuksp1ePRQ?U9}_JSHr;idAaJF=ziDAqF;t%G;ZKQHkzpuY6Fx;H6%8lQ8!Y(Q!YFthlEn{$Rg_p3S4hTg zAZtAFjjS;zpm_TZ>W}_K4YTW)f0AhZduaXd;4MzPs(eCR)arZK9M>)=DPx-@EH`WP_={Br$y_Q4a%c1_JGPD3%T7HUijitn! zmo!U!SKh(vH_fWY^dAwIyvy@;g<6vi<8G$EB}h8$wp~f`kVK`e+wGSMIyBM4B#lKh z-lPpL+8w&`9PuD|?Z4M+^LWn{$WvH3Iglnv2B@{OxYP7}IXyNJWSi&Txfb2g>Z3B| zT|v<%^1UZodN(pja&$w+Z?T1ThGkLK?;W@FX4l5je{_d*(=<=!>z3zdrCI)^f7M%2 zf3LTe{*&Gs_t*a`bFlid`2U%MPoI;)KkN4m?eFCl+aGD`H*+xJt5a(jx`Ry9TrBTC zdzMzPzi5|8?){A;sZYg<4=(Dwd311=DMK2d<^DSxL#N+}{WlvUc)#&Och@HufQ_*Z zurW-!ls#u<33hN8?lMZo(Z`x6sF^Eas>FWRaAJCFxz}}`N6Xs+>%} zH4Oe`d)!r*$$g9S9w4`#TJ8eBw%qmdtKL&^b$LIRRltZ2Q}LL{))42v`eUW3{-bsN0(2O75oRv zLIk-b2T+zBNp}qHlk2Yk=(;zC0)f>f$wePE64h|gO4ztu;xbqyv)b{VN%c7Mmy$cA z@%*9ht=K}T-W8zXM6MmX4l4$XimCAU2p|IzfUQA2C9l}$6Zltv)?gE`H8>*d_I$@M z_+SU9v^F$wmTOvj-y{`}Gq~x{T~OyVS`heOJNV|AiM{C2Y46tbULCA9SFYhTfQwU9 zg|uh~0VBHeJ)R7;mF3&ZN%ZfNPU16l2^-OTtvz=d(-@IdsJtN7m4s*x=;DeNxD`!c zzyzimJ$7;VM~(K7P{B#@j@DA74cn%S;ulo{EBz|$lIk&WcQ>?fT0&q&wr-BfIv-2! z&C%}1Zi2ZyOs!R7TK)v{4n$2?D)0!ZS1KF4N_&EqW4#>}JANC?z$6O?7j6Vt8zf2c zy7Mc6!MCbD6{V^Jpy@; zkZCkAFxrgzdv9=|r{ogo4UTCkh8ud#>(tnjGt)4B$;nTMz%>Dsn=HFk`x7vMVU`v! z%;L%%L793CvqI@5LIVgHR2j8P6=cNuGlmL2$+(EYsEq#E8~pUZ?SSzQgw`KH^zWk=KqJIWifpG+~5`h%JAWNM=L1Lp5%0&?x zzSKC}xU_TL{aYbzhX~$5Y_lnuajnv`kc8aBtRj-?)x|6|2|d$7s?;<>U5~wE&=1ug z#&{*EIPfJa#c8;c%CI6{`aE`<&`7dGCD9T)QjRSlxX7V>pUc^#9>uVf#r?UCi)BP7 z1`M)*A@P4$WU>FAtNs_Z2HzO&oQd*oMVbiES%dYOx!p~1D#!n9HUUe6edm*2O)^cU z+GpY|H#-Zmv~IcFa5vTtzl=v1aufjLkp|#+q~>Wn^5^Qs;xE;Um=Er8{k3%{qx;>& zrs4wE2kzYN#Vwn6?GL;^YViXBd#?-3`kIszJl8YXuiKmNWk`0Zz7(2YRBnFwm0me+u`?IKF;f&x3@ zXJb$kXgb80 zPH`(H@8o#wq~dD@B;gVOWn4xEi7_d0C^f$r7*} zai!c`ne^z>HRKnH{?WtxqYz*VmOX!Idr~&xN50-=F%Ot*?f9+0U#@Iv<1E0Aw0v)N z?+SG1+XE9ZrdCq}*n-A$cngLbJDjmAoddM{jR_*`i86pm)@YuKYgH3n{j(rVJHtxz z$n^E7WKBCV&h~7eymDu<`L)snUA|)Uuazcozy1k^Z5UF+uUPm#oXK)nSO)z=P!c7< z29LI&@K0OtdG%{5w8)O6WAj)yc_c6;;eV43)3kr!c>w^`9sp4QT>aZc)*n#yKWbk5 zU%kkZ|Du|PM{WxoWNkeTvJ!7rKF{^}##PY_J5*G96peXVsA$Cu-NY=o9$Oo3diLQ7 zdXR-VYcDuvzIJg$md~F)8ERazK1T?C_>HT6*A*Q5r7PGQab^)~afp|d>?kVh9T>A< zZ*dXl)~4a}z`<_))v8+fxgRB~m zF+zSbm~+@mTc5KN34S5UHUj$V(pKUP{*%vIxf3Gtxf18`X!hJ}08}AmmK2_8cN&yP zE}H}cP(>YI%l{1W8D7O&sDw*AQ9lgl#JjOCida$}kec(>#`Gy_q3)xLT?69tT#rpb zDQacrd}tEV35LROnEG@R!KMZ!IdExv30G*67DFj@DVl-n@6HBx^eLKfWEkJq-knBM z(vt$Jf(}4GP${)0GaKLs`e~-cN2SA^NZgdp5Ty=~xk^1%1ywQC-|kg4?JT0@HB2EuNFjj01NbE_PHeqiO-U;RM0 zM?cW?&wilc?@Jd}pC#t%)&E?&V4KAg0CE)&R{x%>=;{9FT=iCv>c4Kddsb!gp3xg_ zy#F6G+&x^T7*k&T!4AZN{?!hAiiZn;@h});fusk}4y1*}V{p6{5R_W2T!B>8dp@b{F2iBUwIA(t*~O6w_kO{nvK;?Qc8BT%(^A++L(h$|qP!iH>*#)-0TwUMN4p$_EH5cO#BH0Iax5jZcBg z5uMt>CtZaN&{fQiGrU!ppn675snEazZWL!r%nd+SsoOoj)=k<1tQ?&OufJg}Kdu~i zztFqs*t%lF6dnR1thNQ;!k3ZYI~Wm_Dmw1zR2t~;?W%qx=zx_Yl}|;FtSbLGAtzc! z^BIc5tT-)ns8=zqFTe|QFd;wAWST6!RtRRsk9qV0FBHvbQQ$ZvjyXCC@Eh{_=U$OQ zN^*XGTP;3DL)}m9TJS9PeE|dZ4JMAorWy`c2rTVP(^1KcRyBzBXmhN^u{oR3Yr4!R zF%Ln4TPC7!SIQ9ZY*)%Vd@(0k%+~9gs^@%%f%b15Xu^gdwlr-Q8}z?45IFe~RmYW- z>=DDG0%ju&c8+P{Z}r#WFEj?SQwH)~;2TX9nmK1x0rgu!8ulvH+MZ#%UMZReRJ`w_ zPab>SuO7R<3eXA!V(J%<9e^pC+zuo2#It9pRlxdf+F(IwXrtT5usf2`H>MD0Dpbs z8&-T0lo`|jEr&1?$0|mD2(*;Ob0Fp%rE(fC&a`autwU7jG0jeWmoZf4Hw*y{) z@+)5oSW2%M0jP5L>9C6eQ8oS}Rd*tMV8m$u$*-taBW@IEvg^a8;9Y-J7}L$Y(LHyC zF4DI08S0GYmcj{x%14FCW?i*8MH1_SM1d{}J`6NZSC3Pg5+S&BS}vE1ymm=qJB2bS6$Fk*o-N3=oBzUT3CzrO&L0Fx11z+~jb(`1AK zrXSc3B%v6{s*n|gwCFB2+7Xk6j=>s?gQ3M*#{QRJxUAyC5)fAZSy$2j>60%08xK%G zDErSVN9w=x08;^|I>-9e1B6eSc{a-%J!*Y-F_6vz_)xJykZ~!}t+;wG0*E?5auw1} zEAwq$gMf(JUz5WHFF`uJ54?I-VEq{C%=p%|Wn0Vyf3X=k8GraddlBwiESKT7*@Z^r zb}V3wHHjBUX&WIRDjmiapjpfL@|_c^sB|@;%~a9I#8RDKIIt@KumEKd2yM^R@Vhqo za_e+j@V8Gh#%|s&g&&s2*J+WT=xARn?tE&ky#N$%-=93h0ASVdE3C3qfy?1t>PJ}7 zE64;9S7<#}nWjm#wRRMox9;KM+`j`<6#-CH>78EydbeX8Q<%WD2)Xv;)5y_Vm@|qu z>7K6A5EIg0>F7BO&;krddrE|TrDOXj;7relk2Fk9fvRfB5O;9$D}!6Qts{V{fNhIb z+ypaeBG^S*Sp(^|<0!RB3iSkfzz~bdk69bUHFB}}#~Wcb7Xl|cNq!@;uLO)cT5bA( z18__`Z$fBXGIpnoe8c1DdUb_>UrE8JRg|uT>dPTowilW>jX?l395mu|HMlIKk0oR2jOWm>AX%41E*Pr} z!mAFJ4e%Ath)w5=p8;C8VbggSUx716P-$8=o)#1 zbI~5%iV*gk=qBQFQ4Fo9c)%iGN+P0wLhj41VmZlh|(ZC}7N4b8nVOj5JC9Ojg@G$=tw(;NJ+VKf~&GO{Tw% zt&%>d{`VhsVF(yG4*wtfs0*A(0eNT8Sf9tQq5}VW%8oqZG@;Q;?=SAeR-R{+_{SAn_`0bCt0!YWhq=J+|nx_b+#T4<&TYi>0` zrtI1{>zn(DvR8QdV!2<`UiMZgF7g7#!?FjMmLp)IA<(%b6SVVRK*#!N0ac6Jcb!{x z*HH2J0jEAEbX?h!R)TF;> zzKFrg18lGuzppqkhAa>0&xc&l7%Q(FsDYH5PMMom7O@Mxrq7HM^OS7A<0P6JEDgrC zP15KSc)d+xF=lS3HwD%9GCE{=O`oj+gbX7kT{DQjf#7W>QFR_w=}rzjE^s`&cG4Vk zYZ#$3XVZnZZOGkgG3J`Q*45knc?9K|d9Q3&k)V^Zgt2*V$`6q6=b4$4d!r?>3GOx= z=OjORzO+PP?01$p1?eJJpJ^%98amCZFj4yKzV>}x@AvBp3I?A7x%HYFl)FR;oC)#_d}f^!(-f(L zgV}`RL3=%wi;z{5bcd1ufR;=Y_We0W|HZ3JpPqF zWb)E~1I$EzTN)Z!b)0bT^!$up%mAFXzxK0VoAx3Pkof1KLYT(>R5s~ZK)OhRxlD=m zlg;9-*V2S?$!7%b2C_9?tI&`*VG<&f<#Du%YO?L!P>7et`-&%3^Rp$qgg;t%ud5F{ zFCF`FvBjhF2bRnPU#>8Y_9_}XDBo!Pm78K7&NrU#za0D``s*hs->8SpH+qlu&n@R0 z?VwnGaj#kfoNr{3JMQKiuPGFdPo?CQ0@963V=>a@(EuScW8l8^>$b#q0CfQ)WR_7E zc>r}`luU#nGdU1jPM)^(Um`rwI{LyyDAdG?KxmoFM8{$jy^p^-Bx37!k`!u)%(OQU zLdb-J%T+-JxBF`mV6<=nMvJ25kAXn~;pZjP#wBlOGAG<_>0#v{QkAOBJjUtfU31(1 znDru!**Hsfv@$;@S0A_lI^1w20O`h%_aNN}j26kHEnjp78$X9|x<~lC9=Uw#tucq# z@loSof+IJma#QB#xC;5 zRF3k?c>c3zZ#o|<>}hu`J|x}55Fm7OP~yp2*DL2wKmDK(*6%9TFQ2U6M~9ay*54Tf z|7R8JucO1l|5#2;;IrKb-x}k8FFkGYYfQNSf6mRFySd-Z9wOe+lEE`o>i$e_LTWk! zZdwf7|91BTxvdXZ2yQp964eb9hYd_$AmD}RNsHxhqvgS`G0nvu8<7)ckw?F-3HkM zHyRn&RRenEwZ;xx05n zUyT?x2kf3uvVGcIF8D5BknFAbxY3pr($4Z%l`b3?UF3$J!hEKJD6vC;&(zd7>dEsZ zIAXPEy}Lap))X3b`N9JZl95Q(P~G77d~vNKk^juYd?qc3&ty4WJS7tMN&)6Gu^0An zoWIbj?V%+K_)MlxT5k!K>41bI;4^`Q<1(Mg7{g~$Li3qY!0hVO%IqrX<60=081bn* z$R7BcqJ@*n|GU}M{|o)VE0@@$zh^4`dv^7#+RYc9UCDiV>DwnaM+x$c7W3+vrMYK$ zCkgTq7A6IWRSVM+#=;c5*Xq*TQ1*mZMDAF{=#Tl>2uxxmL-8dtG4d2nj7%*jMu7D; z=0h#6^;TdPlo$y+v(dl%6GgvfK|E*)!d2@FZM1Y1RJA${+@Y4>D)?;8mY{TaRz(EC zG+k z7f{u*pig36Mhf2I>o8mNtowv?o^tKhWaz|6{74J(A>)9ss zroQ0X>n#{kr;(qmeRD^4o2+<6=h)@WZ)cpzpIX-(>6MG|H?N_Im$Mq~0(Og|#~g)5 zCO@$sQDuEFSQ0hh8_LDpI!<>1;V-cBTL~^)jB{?j#>$~b`U&W#X*RQi)YD zgCA&L?N43myIO0~yVd)%pSDJJuM{RZJO?L-uU-Vn;qLB;Znf~yu~JZS@|2VAuPO-38gWH41L1Fl-an5)(*O-3zI zSKDR5)*#k!R&$doxfbMVFDpy2+53s_QPuGeb%m53ZLsCyUc;T&)HiAaRXOJ=@E5zn zPP{dw5kXchFdSZ^YQe){gI<*#oX~K1WL7HX+rvhIsZ#T0noQ;4nPE!{K$F=}ZVAz3 zBwILy%Pp}qndhrCnb#%_p@1e+p@^l)@B*kfQCzdo1UF|G8$SR9rVR% zxcl^JQg58ZFA25F0su6XrYC!!m#jWojwA^)bvZWT3KAa}+O|r>F>5Bx&Ycq8*3Mt# z$u6?WlF@`%GKAKe316r0wF_oOhFb2nrJyKarB`DKzx~3Y&BDWsaZFaLckz;CJcAlZ z<=g45t%-3xT=Ps79`aSbKMl2*gCLH~;GkQ^ZXa)or&&Wk+5tyqX=~rgY?xaTV`qmy z4KCx)<`V7${255(D+Bx)oXGcu68X;t=R9AX1&RE9^(J4oR+1qS`Bp25eC2H*EFu;S zEVoA!OsqD{D_0X*X$WvYS&`V3^IpOY%YKXC%Pe>Kov$32cZcp-ewWKM^~uEbuQN4v z4>aO0l6LmANO<7bo&8jdP!j8yWOuL}7t`^Vd{OtSOuI^U7G4U`^lIb*w2P*E z_v#CMrQu8XAS==UXGI?AwO@GV6S8c%IJcAqJ`1i|E+$~hh3hloh8@!v`l>W5=uSX^ ze3~88g!!a96N)WQT5+9>2R^2#gqAs-St{Wg*GY4xQA-(+#PUoUZfKgQdqo*IMkxwt z7l11$Qm%-#T)>X@<*3LE6cy3JjlX? z#3EOz+EA{F-)EYGF2CYFTmM_`H@wpRj4K_lpLDs@Zlv)hnFV z;1g79(A-AQrqVzT`Yb5l-OpLMR#b#OYf9!1ZdtgYd<5zOxY?6O)<#{B@WQBzSRIiN z&Nm5*gO%dpm=IP**KqMs+BuJ}ctaB)g!Pqo=-%9kWQREr!qUoV>@bgYm~R4f>Br#- z%*_VItn+G9+JG({*KJv3SR*}`K{gsGD_!dzwZ~1tXdgGBM_93OC`*p|3lPFO-~;N_ z>th{`7hMO16B%8Z&dS<9x~`w(@92J`ZO4r-Gb2ptZD1rDz(WAp*#jUu`(XV??S%== z2NTM(idVrX%PFe*P}jRs5X>*a1oIVyTEjHArolB!KTTs&TJJ-k*V5P0G?{I`^#~$6 z7#L(HM3KqF35rxhL6K9~ph&I{u?%idgtZ9@iacZ|Nxw(FLlwmGa}MPS!!eP9jJQaM zeAc2a$9xD&K1+tkXD+_tPy$PXops(A&@M`eh1~Wz*M8q1vM_5akYJUYhRVx1f$Y+K(+4TOTH;8(){cFR!vQg zHr(?!bSCt^bAz~uDijyl?afmR(Cv~(KBqwF_O507Mc!)F;vkPn{j?1j+nEDG`#otD zI?P{^{;O(jK9p&x%}n9Azb(kUT33!hz513^P^3u_C+N#(%Zv{=@?UIk=q$%eA#In` zHx~z1K}XByk#KZ44Oj&az|rBbZ%_Sr7cd=+_I_#sburo%KGU9MKGSLk!y4PcnAWX4 z0h0M@*bYWD=wO&RX7NQ`Gq1z&nS@o@EzEo8p$-N_72H%F0S~kc(jM|rk}6r|Syvj| zg4pdbS`KUX2G1W!9{Pi-g_Fzwzt$Uk(e%bH{$rQMopdHov5jvy!;|5gmfK1Aru724 zY2DbV*fEg7D000~CTkIzXib5M)>m0*qBS>rk5}n+OG~)=_vaof6RoxEf>kSaL2img zB(d|~6D`S)xWrD3U638J3;J5FT(xXAe8;_N$;4x0`QlJ4f9$Ixbk(|cKz_17I-K^O2|9gY~ zgWe#8h*8s@^agPrt-tRL{-3vB3yuV@A*Cgk2{3B%BJw}WZG7ZXfX{OySn1zu4? zp%md|iZ5iLvbD|8Z4+e0-7Ze(`w(lzU0yf>thm>K71s$yUepZiI3mn8+Br{}l5Ycf zan(l2E-xc5O7KdQ0!wh4gO{H)#9oFrj$q1`HYi)k%VoxkH)h9=_l#!rm^;=T`txZ5&it1ZX!0SuR9l*MaWW-86|8SngbwueI1 zWXxB=|COTkZ_G5dXu0|J{KpQoW*8DO)$LpMcxRHt09xnvrFh_RH{9tPO1l45#mBon z#wD=F-DT#KjFq6!jWiV>OO|gs?ERkl7s$jt^(P-h&kh5Ry9w~P&s_9&auv8TO*n4i zI9O#~4^|_rdfefgmJ950AB%H0#__mAi0MD|xT82SYkAz?{)xvO*ap||xC>X47ye0a z5a+S_Zz;6rtagR4K#5M9O$5NfiB)oMg3qWwUl$8y%nu{0GFQ!$6Cd8C^Lpw)` z6Af}ssZ*#P%CypErgGfh5#;WYoko;A7CXC5)vGD7DA5S)9Qp7F@(j|e^Vc>JzU9(4 zKg55~XdGgMem6I};6J)H{j1&cQN8wly-^F*XPi%c*O z;*x>b?I$31JIB~yzq;=xnOT_KzBe0`tA~8Pbh>*0yB#p)H!G-FO+f5+i>s^bc1*c? zf@ZfjA^}bP0MO)AT!~(cu**(XcO{j-P9h68l>dLMH|Urm6=24xAtR-m|C?);u}H4h zEf_JaU!~1_5wRA50gSW0LgTD2E8{F^n-6kKTneKuAcl-ihjaOY9^3ta_>G589vt%R zuyH6}tYrSoOj5H@pJ!TK&s2ZCyXTZO#*mv-6%MbS8>ZN(-KA`;&}2>&7&GC<*?D%v zP;=pv_AJ!D=;a;{IEh8bbU_H2d*lEi^I-JkuJmdEyZEsndwcr8)-uy`k4*fgI11nE zim+Ia#p;L??I^<=W(#oIq#O3JQ}+EWdL=8~1{R_F{gerSE9j9($*g{40N|yMuHdCL z0ABh*ULkc26$UTOXl`EnklqoNyFiO{Q{MDGn|S>Dwr5qmXaoJD*jpDy=Kj5pXf0%lnaPpZW4)1^YDLG6p) z{?1Uj+G)5NAD)4zf(h^@AH=>9kH6SAVu;u`0!)LbeIpijmO5aQ&%gGKShP}F;F8<# zu1#(av7@Q{lX``Eu8@sEf!wPU=^1)=H>$SNs%)aHqhOAh{ZPR~P&d2Xy^5ZT=Sq;( z+3ldzb6Qto{z8#YuzQv8u zSPg`-cwJ15jvD^csr`I3VbeXn>*MJ|eQtEOvSqA1Oe%Nu>RvA(rWgu-gEP!3oW=~Z z;7}HXZO06=N+2@R@-VBnRf<8UMz4Hfn)Uv%OB?5H4)#iyT03MdRuFzBAE{no&oeC! zV5+|Edi&WU$b35Rdgk3F4>N1o7_G1hGDD!J<=w6U4WB&SPxul*}r{+z6X{+U+{nF?e|h zXGc~yLtG}dUTNP!1&Fn$ufaUQ+qAq%M*=CSP9;2iyzym+aN592o-F)~@>|k@ktPLq z^r8Ul;&YV{RIDyWkvcj*_|kNXCyF$hlram2U7Q?00bmy_Fzg}#?g}2{npYVNhhZ0D zv^0CieC=?*3tqyID1>7?bAXZXRv!D31akXC2C|ZH8V2G9T z3{j}FkiQjEb2UMHKmu);^lS2#;el8x6)!?fZ;;sb2S?GZUg&_?5u0` z0Zf^q(mC`S?n($Shh4YA?{{Sq`7HiLR>8aQ<&}qy4d${-9<$rrv`UVjVz0<^aeXaP zVOJe~!yzwVA9vV0wwU+>kzDa2H$`Zll1(IyDL(Tfn2RhhHsosUxf{RB#O!@bt7u4E z=KX_Nr9K)Q-Jb+iCspP?9~5-o6|#M>pi23kDzl%&7q|J>N*(SN(s7KB$U8db2A%JW zy=k-isr@S4KGxxsrf1+!_lb|Dr^i$aezxPCE9Od#JKTMKVWaB!Rheq7kJ~m}*)N_% z(03DGN|}o#o$6F4G13j%V0`PPwh0arHg4$ zHOl8ZJ?oy&VG$|Xr@^KAX1qP4Pu}L-iEclIgJ0uh?mW%t*-_rjCF08yU!zWbp-ptt z&EU5$ZaWP>nlRBKPToMqPg|BnNKaqeHIqio+#i@|da>or$2>Ei!X~Y+yXYuK>Az<{2y!+Ak!-gtY?W+$>S@t5Zp?bIfj zsNzT7*=EZf2`dmyGS4_&Vz7Raco?xnrPJ2RFT8I$;~ zG<&GA%5kR~LwoZu^8FI@zF=yPu`r zE@4kWZ*17dD=)(wnrP=_CPyJ%#l}9UCa{L5ae;$oQVWPd5{=p8>o;pLGEe3oe^k0+R^Sb8i&pAn^ z+a^-!E|IoMSXC6MkLUA}#8qz4H!o4iVZRfrX)}LCwcE~5v@p-U=}l>ht|Yn91^#dD z(=C&AY9&4G1<{L}28Q-$)JsuJ*j4qUkWdf1{os70`*!g@@1>tk?D|^1(o@713blgQ zLT|m?)@Ua3{fy)TUlRJC_OTXpZ4dD7)N*WX77#fuh;M$5Dmt`TFO#`N@`}?~%VEPu zWfH>WFOxm&Xy!Sc8Ac{-Z>sBtRnlaezdijtk*ZEb(n~^%LCr;`Ayz0U$lyJBV43&m zcOLuA$q`rk0`DK@n0sMqdh}NC$-_Fv6@9^{GQ3$OkJDs2^1X zvER{;e!95WL2CcOAuV|yj`ZXvae`VQrbG_WCaq@ZxyRo?jYkb$Y7Cbz?3hhh;buI% z1Csc7Tfo1r{3`N?{-)?);bw1j+Lix|y&d{1M^#R>Ol-iz+s}@Nw{zvoz@J6_13*vz zq_dTxn}w^XyqSxYrk$Cq6_!AORtm_@ef$8UcHGGyvB+U#62LwJB>ad zdfy;c@5?TP9_oPW2%>kaVFmSeBLr~|h;YiG4~SkZh1JXDMd+apxQ-xtpAJ@#Q~)7} zdq9LQ0)0UADiN&SaUp~r>VWGAqE{$j1{dYt;vNuz@IoIDO##E|aUVwLp$@o?Aew)K6+EYj5X3nklG}nlAR7IG z)q8dXp@)0G^#sw-3#=fYK0*-pfCvNz`he)TKUVL&0YVRTz;y)CF=wn`zcE4(_kioX z@uSn7SUnLlgdXaE>jYWTm=%Eg{jv%_tgcYm{LkRw&>4bh4L^mw3YLBk0sfK7@ptaC< sV60Y6_?lYC8?mMuArbTiyx_McxI|Y+;^A$>JK$_(r68xsvT}d=FPEyM`v3p{ diff --git a/data/examples/qet_cabinet_assets/qet_wire_duct.step b/data/examples/qet_cabinet_assets/qet_wire_duct.step index f4ee120..3e81231 100644 --- a/data/examples/qet_cabinet_assets/qet_wire_duct.step +++ b/data/examples/qet_cabinet_assets/qet_wire_duct.step @@ -1,7 +1,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); -FILE_NAME('Open CASCADE Shape Model','2026-05-26T19:26:07',(''),(''), +FILE_NAME('Open CASCADE Shape Model','2026-05-31T14:15:58',(''),(''), 'Open CASCADE STEP processor 7.8','FreeCAD','Unknown'); FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); ENDSEC; @@ -14,7376 +14,5467 @@ DATA; #4 = PRODUCT_DEFINITION_SHAPE('','',#5); #5 = PRODUCT_DEFINITION('design','',#6,#9); #6 = PRODUCT_DEFINITION_FORMATION('','',#7); -#7 = PRODUCT('QETWireDuct','QETWireDuct','',(#8)); +#7 = PRODUCT('WireDuct_Body','WireDuct_Body','',(#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,#55,#59,#63,#67,#71,#75,#79,#83,#87,#91,#95,#99,#103,#107,#111, - #115,#119,#123,#127,#131,#135,#139,#143,#147,#151,#155,#159,#163, - #167,#171,#175,#179),#183); +#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#5418); #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.)); +#15 = MANIFOLD_SOLID_BREP('',#16); +#16 = CLOSED_SHELL('',(#17,#57,#88,#119,#150,#174,#198,#798,#822,#1420, + #1477,#1508,#1565,#1582,#1599,#1616,#1633,#1650,#1667,#1684,#1701, + #1718,#1735,#1752,#1769,#1786,#1803,#1820,#1837,#1854,#1871,#1888, + #1905,#1922,#1939,#1956,#1973,#1990,#2007,#2024,#2041,#2058,#2075, + #2092,#2109,#2126,#2143,#2160,#2177,#2194,#2211,#2228,#2245,#2262, + #2279,#2296,#2313,#2330,#2347,#2364,#2381,#2398,#2415,#2432,#2449, + #2466,#2483,#2500,#2517,#2534,#2551,#2568,#2585,#2602,#2619,#2636, + #2653,#2670,#2687,#2704,#2721,#2738,#2755,#2772,#2789,#2806,#2818, + #3425,#3442,#3459,#3476,#3500,#3531,#3548,#3565,#3589,#3613,#3637, + #3661,#3685,#3709,#3733,#3757,#3781,#3805,#3829,#3853,#3877,#3901, + #3925,#3949,#3973,#3997,#4021,#4045,#4069,#4093,#4117,#4141,#4165, + #4189,#4213,#4237,#4261,#4285,#4309,#4333,#4357,#4381,#4405,#4429, + #4453,#4477,#4501,#4525,#4549,#4573,#4597,#4621,#4645,#4669,#4693, + #4717,#4741,#4765,#4789,#4813,#4837,#4861,#4885,#4909,#4933,#4957, + #4981,#5005,#5029,#5053,#5077,#5101,#5125,#5149,#5173,#5197,#5221, + #5245,#5269,#5293,#5317,#5334)); +#17 = ADVANCED_FACE('',(#18),#52,.F.); +#18 = FACE_BOUND('',#19,.F.); +#19 = EDGE_LOOP('',(#20,#30,#38,#46)); +#20 = ORIENTED_EDGE('',*,*,#21,.F.); +#21 = EDGE_CURVE('',#22,#24,#26,.T.); +#22 = VERTEX_POINT('',#23); +#23 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#24 = VERTEX_POINT('',#25); +#25 = CARTESIAN_POINT('',(-100.,-20.,2.)); +#26 = LINE('',#27,#28); +#27 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#28 = VECTOR('',#29,1.); #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.)); +#30 = ORIENTED_EDGE('',*,*,#31,.T.); +#31 = EDGE_CURVE('',#22,#32,#34,.T.); +#32 = VERTEX_POINT('',#33); +#33 = CARTESIAN_POINT('',(-100.,-18.,0.)); +#34 = LINE('',#35,#36); +#35 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#36 = VECTOR('',#37,1.); +#37 = DIRECTION('',(-0.,1.,0.)); +#38 = ORIENTED_EDGE('',*,*,#39,.T.); +#39 = EDGE_CURVE('',#32,#40,#42,.T.); +#40 = VERTEX_POINT('',#41); +#41 = CARTESIAN_POINT('',(-100.,-18.,2.)); +#42 = LINE('',#43,#44); +#43 = CARTESIAN_POINT('',(-100.,-18.,0.)); +#44 = VECTOR('',#45,1.); #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 = AXIS2_PLACEMENT_3D('',#52,#53,#54); -#52 = CARTESIAN_POINT('',(0.,0.,0.)); -#53 = DIRECTION('',(0.,0.,1.)); -#54 = DIRECTION('',(1.,0.,0.)); -#55 = AXIS2_PLACEMENT_3D('',#56,#57,#58); -#56 = CARTESIAN_POINT('',(0.,0.,0.)); -#57 = DIRECTION('',(0.,0.,1.)); -#58 = DIRECTION('',(1.,0.,0.)); -#59 = AXIS2_PLACEMENT_3D('',#60,#61,#62); -#60 = CARTESIAN_POINT('',(0.,0.,0.)); -#61 = DIRECTION('',(0.,0.,1.)); -#62 = DIRECTION('',(1.,0.,0.)); -#63 = AXIS2_PLACEMENT_3D('',#64,#65,#66); -#64 = CARTESIAN_POINT('',(0.,0.,0.)); -#65 = DIRECTION('',(0.,0.,1.)); -#66 = DIRECTION('',(1.,0.,0.)); -#67 = AXIS2_PLACEMENT_3D('',#68,#69,#70); -#68 = CARTESIAN_POINT('',(0.,0.,0.)); -#69 = DIRECTION('',(0.,0.,1.)); -#70 = DIRECTION('',(1.,0.,0.)); -#71 = AXIS2_PLACEMENT_3D('',#72,#73,#74); -#72 = CARTESIAN_POINT('',(0.,0.,0.)); -#73 = DIRECTION('',(0.,0.,1.)); -#74 = DIRECTION('',(1.,0.,0.)); -#75 = AXIS2_PLACEMENT_3D('',#76,#77,#78); -#76 = CARTESIAN_POINT('',(0.,0.,0.)); -#77 = DIRECTION('',(0.,0.,1.)); -#78 = DIRECTION('',(1.,0.,0.)); -#79 = AXIS2_PLACEMENT_3D('',#80,#81,#82); -#80 = CARTESIAN_POINT('',(0.,0.,0.)); -#81 = DIRECTION('',(0.,0.,1.)); -#82 = DIRECTION('',(1.,0.,0.)); -#83 = AXIS2_PLACEMENT_3D('',#84,#85,#86); -#84 = CARTESIAN_POINT('',(0.,0.,0.)); -#85 = DIRECTION('',(0.,0.,1.)); -#86 = DIRECTION('',(1.,0.,0.)); -#87 = AXIS2_PLACEMENT_3D('',#88,#89,#90); -#88 = CARTESIAN_POINT('',(0.,0.,0.)); -#89 = DIRECTION('',(0.,0.,1.)); -#90 = DIRECTION('',(1.,0.,0.)); -#91 = AXIS2_PLACEMENT_3D('',#92,#93,#94); -#92 = CARTESIAN_POINT('',(0.,0.,0.)); -#93 = DIRECTION('',(0.,0.,1.)); -#94 = DIRECTION('',(1.,0.,0.)); -#95 = AXIS2_PLACEMENT_3D('',#96,#97,#98); -#96 = CARTESIAN_POINT('',(0.,0.,0.)); -#97 = DIRECTION('',(0.,0.,1.)); -#98 = DIRECTION('',(1.,0.,0.)); -#99 = AXIS2_PLACEMENT_3D('',#100,#101,#102); -#100 = CARTESIAN_POINT('',(0.,0.,0.)); -#101 = DIRECTION('',(0.,0.,1.)); -#102 = DIRECTION('',(1.,0.,0.)); -#103 = AXIS2_PLACEMENT_3D('',#104,#105,#106); -#104 = CARTESIAN_POINT('',(0.,0.,0.)); -#105 = DIRECTION('',(0.,0.,1.)); -#106 = DIRECTION('',(1.,0.,0.)); -#107 = AXIS2_PLACEMENT_3D('',#108,#109,#110); -#108 = CARTESIAN_POINT('',(0.,0.,0.)); -#109 = DIRECTION('',(0.,0.,1.)); -#110 = DIRECTION('',(1.,0.,0.)); -#111 = AXIS2_PLACEMENT_3D('',#112,#113,#114); -#112 = CARTESIAN_POINT('',(0.,0.,0.)); -#113 = DIRECTION('',(0.,0.,1.)); -#114 = DIRECTION('',(1.,0.,0.)); +#46 = ORIENTED_EDGE('',*,*,#47,.F.); +#47 = EDGE_CURVE('',#24,#40,#48,.T.); +#48 = LINE('',#49,#50); +#49 = CARTESIAN_POINT('',(-100.,-20.,2.)); +#50 = VECTOR('',#51,1.); +#51 = DIRECTION('',(-0.,1.,0.)); +#52 = PLANE('',#53); +#53 = AXIS2_PLACEMENT_3D('',#54,#55,#56); +#54 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#55 = DIRECTION('',(1.,0.,-0.)); +#56 = DIRECTION('',(0.,0.,1.)); +#57 = ADVANCED_FACE('',(#58),#83,.F.); +#58 = FACE_BOUND('',#59,.F.); +#59 = EDGE_LOOP('',(#60,#68,#69,#77)); +#60 = ORIENTED_EDGE('',*,*,#61,.F.); +#61 = EDGE_CURVE('',#22,#62,#64,.T.); +#62 = VERTEX_POINT('',#63); +#63 = CARTESIAN_POINT('',(100.,-20.,0.)); +#64 = LINE('',#65,#66); +#65 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#66 = VECTOR('',#67,1.); +#67 = DIRECTION('',(1.,0.,-0.)); +#68 = ORIENTED_EDGE('',*,*,#21,.T.); +#69 = ORIENTED_EDGE('',*,*,#70,.T.); +#70 = EDGE_CURVE('',#24,#71,#73,.T.); +#71 = VERTEX_POINT('',#72); +#72 = CARTESIAN_POINT('',(100.,-20.,2.)); +#73 = LINE('',#74,#75); +#74 = CARTESIAN_POINT('',(-100.,-20.,2.)); +#75 = VECTOR('',#76,1.); +#76 = DIRECTION('',(1.,0.,-0.)); +#77 = ORIENTED_EDGE('',*,*,#78,.F.); +#78 = EDGE_CURVE('',#62,#71,#79,.T.); +#79 = LINE('',#80,#81); +#80 = CARTESIAN_POINT('',(100.,-20.,0.)); +#81 = VECTOR('',#82,1.); +#82 = DIRECTION('',(0.,0.,1.)); +#83 = PLANE('',#84); +#84 = AXIS2_PLACEMENT_3D('',#85,#86,#87); +#85 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#86 = DIRECTION('',(-0.,1.,0.)); +#87 = DIRECTION('',(0.,0.,1.)); +#88 = ADVANCED_FACE('',(#89),#114,.F.); +#89 = FACE_BOUND('',#90,.F.); +#90 = EDGE_LOOP('',(#91,#99,#100,#108)); +#91 = ORIENTED_EDGE('',*,*,#92,.F.); +#92 = EDGE_CURVE('',#24,#93,#95,.T.); +#93 = VERTEX_POINT('',#94); +#94 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#95 = LINE('',#96,#97); +#96 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#97 = VECTOR('',#98,1.); +#98 = DIRECTION('',(0.,0.,1.)); +#99 = ORIENTED_EDGE('',*,*,#47,.T.); +#100 = ORIENTED_EDGE('',*,*,#101,.T.); +#101 = EDGE_CURVE('',#40,#102,#104,.T.); +#102 = VERTEX_POINT('',#103); +#103 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#104 = LINE('',#105,#106); +#105 = CARTESIAN_POINT('',(-100.,-18.,0.)); +#106 = VECTOR('',#107,1.); +#107 = DIRECTION('',(0.,0.,1.)); +#108 = ORIENTED_EDGE('',*,*,#109,.F.); +#109 = EDGE_CURVE('',#93,#102,#110,.T.); +#110 = LINE('',#111,#112); +#111 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#112 = VECTOR('',#113,1.); +#113 = DIRECTION('',(-0.,1.,0.)); +#114 = PLANE('',#115); #115 = AXIS2_PLACEMENT_3D('',#116,#117,#118); -#116 = CARTESIAN_POINT('',(0.,0.,0.)); -#117 = DIRECTION('',(0.,0.,1.)); -#118 = DIRECTION('',(1.,0.,0.)); -#119 = AXIS2_PLACEMENT_3D('',#120,#121,#122); -#120 = CARTESIAN_POINT('',(0.,0.,0.)); -#121 = DIRECTION('',(0.,0.,1.)); -#122 = DIRECTION('',(1.,0.,0.)); -#123 = AXIS2_PLACEMENT_3D('',#124,#125,#126); -#124 = CARTESIAN_POINT('',(0.,0.,0.)); -#125 = DIRECTION('',(0.,0.,1.)); -#126 = DIRECTION('',(1.,0.,0.)); -#127 = AXIS2_PLACEMENT_3D('',#128,#129,#130); -#128 = CARTESIAN_POINT('',(0.,0.,0.)); -#129 = DIRECTION('',(0.,0.,1.)); -#130 = DIRECTION('',(1.,0.,0.)); -#131 = AXIS2_PLACEMENT_3D('',#132,#133,#134); -#132 = CARTESIAN_POINT('',(0.,0.,0.)); -#133 = DIRECTION('',(0.,0.,1.)); -#134 = DIRECTION('',(1.,0.,0.)); -#135 = AXIS2_PLACEMENT_3D('',#136,#137,#138); -#136 = CARTESIAN_POINT('',(0.,0.,0.)); -#137 = DIRECTION('',(0.,0.,1.)); -#138 = DIRECTION('',(1.,0.,0.)); -#139 = AXIS2_PLACEMENT_3D('',#140,#141,#142); -#140 = CARTESIAN_POINT('',(0.,0.,0.)); -#141 = DIRECTION('',(0.,0.,1.)); -#142 = DIRECTION('',(1.,0.,0.)); -#143 = AXIS2_PLACEMENT_3D('',#144,#145,#146); -#144 = CARTESIAN_POINT('',(0.,0.,0.)); -#145 = DIRECTION('',(0.,0.,1.)); -#146 = DIRECTION('',(1.,0.,0.)); -#147 = AXIS2_PLACEMENT_3D('',#148,#149,#150); -#148 = CARTESIAN_POINT('',(0.,0.,0.)); +#116 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#117 = DIRECTION('',(1.,0.,-0.)); +#118 = DIRECTION('',(0.,0.,1.)); +#119 = ADVANCED_FACE('',(#120),#145,.F.); +#120 = FACE_BOUND('',#121,.F.); +#121 = EDGE_LOOP('',(#122,#123,#131,#139)); +#122 = ORIENTED_EDGE('',*,*,#39,.F.); +#123 = ORIENTED_EDGE('',*,*,#124,.T.); +#124 = EDGE_CURVE('',#32,#125,#127,.T.); +#125 = VERTEX_POINT('',#126); +#126 = CARTESIAN_POINT('',(-100.,18.,0.)); +#127 = LINE('',#128,#129); +#128 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#129 = VECTOR('',#130,1.); +#130 = DIRECTION('',(-0.,1.,0.)); +#131 = ORIENTED_EDGE('',*,*,#132,.T.); +#132 = EDGE_CURVE('',#125,#133,#135,.T.); +#133 = VERTEX_POINT('',#134); +#134 = CARTESIAN_POINT('',(-100.,18.,2.)); +#135 = LINE('',#136,#137); +#136 = CARTESIAN_POINT('',(-100.,18.,0.)); +#137 = VECTOR('',#138,1.); +#138 = DIRECTION('',(0.,0.,1.)); +#139 = ORIENTED_EDGE('',*,*,#140,.F.); +#140 = EDGE_CURVE('',#40,#133,#141,.T.); +#141 = LINE('',#142,#143); +#142 = CARTESIAN_POINT('',(-100.,-20.,2.)); +#143 = VECTOR('',#144,1.); +#144 = DIRECTION('',(-0.,1.,0.)); +#145 = PLANE('',#146); +#146 = AXIS2_PLACEMENT_3D('',#147,#148,#149); +#147 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#148 = DIRECTION('',(1.,0.,-0.)); #149 = DIRECTION('',(0.,0.,1.)); -#150 = DIRECTION('',(1.,0.,0.)); -#151 = AXIS2_PLACEMENT_3D('',#152,#153,#154); -#152 = CARTESIAN_POINT('',(0.,0.,0.)); -#153 = DIRECTION('',(0.,0.,1.)); -#154 = DIRECTION('',(1.,0.,0.)); -#155 = AXIS2_PLACEMENT_3D('',#156,#157,#158); -#156 = CARTESIAN_POINT('',(0.,0.,0.)); -#157 = DIRECTION('',(0.,0.,1.)); -#158 = DIRECTION('',(1.,0.,0.)); -#159 = AXIS2_PLACEMENT_3D('',#160,#161,#162); -#160 = CARTESIAN_POINT('',(0.,0.,0.)); -#161 = DIRECTION('',(0.,0.,1.)); -#162 = DIRECTION('',(1.,0.,0.)); -#163 = AXIS2_PLACEMENT_3D('',#164,#165,#166); -#164 = CARTESIAN_POINT('',(0.,0.,0.)); -#165 = DIRECTION('',(0.,0.,1.)); -#166 = DIRECTION('',(1.,0.,0.)); -#167 = AXIS2_PLACEMENT_3D('',#168,#169,#170); -#168 = CARTESIAN_POINT('',(0.,0.,0.)); -#169 = DIRECTION('',(0.,0.,1.)); -#170 = DIRECTION('',(1.,0.,0.)); -#171 = AXIS2_PLACEMENT_3D('',#172,#173,#174); -#172 = CARTESIAN_POINT('',(0.,0.,0.)); -#173 = DIRECTION('',(0.,0.,1.)); -#174 = DIRECTION('',(1.,0.,0.)); -#175 = AXIS2_PLACEMENT_3D('',#176,#177,#178); -#176 = CARTESIAN_POINT('',(0.,0.,0.)); -#177 = DIRECTION('',(0.,0.,1.)); -#178 = DIRECTION('',(1.,0.,0.)); -#179 = AXIS2_PLACEMENT_3D('',#180,#181,#182); -#180 = CARTESIAN_POINT('',(0.,0.,0.)); -#181 = DIRECTION('',(0.,0.,1.)); -#182 = DIRECTION('',(1.,0.,0.)); -#183 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#187)) GLOBAL_UNIT_ASSIGNED_CONTEXT -((#184,#185,#186)) REPRESENTATION_CONTEXT('Context #1', - '3D Context with UNIT and UNCERTAINTY') ); -#184 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#185 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#186 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#187 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#184, - 'distance_accuracy_value','confusion accuracy'); -#188 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); -#189 = SHAPE_DEFINITION_REPRESENTATION(#190,#196); -#190 = PRODUCT_DEFINITION_SHAPE('','',#191); -#191 = PRODUCT_DEFINITION('design','',#192,#195); -#192 = PRODUCT_DEFINITION_FORMATION('','',#193); -#193 = PRODUCT('WireDuct_BasePlate','WireDuct_BasePlate','',(#194)); -#194 = PRODUCT_CONTEXT('',#2,'mechanical'); -#195 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#196 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#197),#347); -#197 = MANIFOLD_SOLID_BREP('',#198); -#198 = CLOSED_SHELL('',(#199,#239,#279,#301,#323,#335)); -#199 = ADVANCED_FACE('',(#200),#234,.F.); -#200 = FACE_BOUND('',#201,.F.); -#201 = EDGE_LOOP('',(#202,#212,#220,#228)); -#202 = ORIENTED_EDGE('',*,*,#203,.F.); -#203 = EDGE_CURVE('',#204,#206,#208,.T.); -#204 = VERTEX_POINT('',#205); -#205 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#206 = VERTEX_POINT('',#207); -#207 = CARTESIAN_POINT('',(-100.,-20.,2.)); -#208 = LINE('',#209,#210); -#209 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#210 = VECTOR('',#211,1.); -#211 = DIRECTION('',(0.,0.,1.)); -#212 = ORIENTED_EDGE('',*,*,#213,.T.); -#213 = EDGE_CURVE('',#204,#214,#216,.T.); -#214 = VERTEX_POINT('',#215); -#215 = CARTESIAN_POINT('',(-100.,20.,0.)); -#216 = LINE('',#217,#218); -#217 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#218 = VECTOR('',#219,1.); -#219 = DIRECTION('',(-0.,1.,0.)); -#220 = ORIENTED_EDGE('',*,*,#221,.T.); -#221 = EDGE_CURVE('',#214,#222,#224,.T.); -#222 = VERTEX_POINT('',#223); -#223 = CARTESIAN_POINT('',(-100.,20.,2.)); -#224 = LINE('',#225,#226); -#225 = CARTESIAN_POINT('',(-100.,20.,0.)); -#226 = VECTOR('',#227,1.); -#227 = DIRECTION('',(0.,0.,1.)); -#228 = ORIENTED_EDGE('',*,*,#229,.F.); -#229 = EDGE_CURVE('',#206,#222,#230,.T.); -#230 = LINE('',#231,#232); -#231 = CARTESIAN_POINT('',(-100.,-20.,2.)); -#232 = VECTOR('',#233,1.); -#233 = DIRECTION('',(-0.,1.,0.)); -#234 = PLANE('',#235); -#235 = AXIS2_PLACEMENT_3D('',#236,#237,#238); -#236 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#237 = DIRECTION('',(1.,0.,-0.)); -#238 = DIRECTION('',(0.,0.,1.)); -#239 = ADVANCED_FACE('',(#240),#274,.T.); -#240 = FACE_BOUND('',#241,.T.); -#241 = EDGE_LOOP('',(#242,#252,#260,#268)); -#242 = ORIENTED_EDGE('',*,*,#243,.F.); -#243 = EDGE_CURVE('',#244,#246,#248,.T.); -#244 = VERTEX_POINT('',#245); -#245 = CARTESIAN_POINT('',(100.,-20.,0.)); -#246 = VERTEX_POINT('',#247); -#247 = CARTESIAN_POINT('',(100.,-20.,2.)); -#248 = LINE('',#249,#250); -#249 = CARTESIAN_POINT('',(100.,-20.,0.)); -#250 = VECTOR('',#251,1.); -#251 = DIRECTION('',(0.,0.,1.)); -#252 = ORIENTED_EDGE('',*,*,#253,.T.); -#253 = EDGE_CURVE('',#244,#254,#256,.T.); -#254 = VERTEX_POINT('',#255); -#255 = CARTESIAN_POINT('',(100.,20.,0.)); -#256 = LINE('',#257,#258); -#257 = CARTESIAN_POINT('',(100.,-20.,0.)); -#258 = VECTOR('',#259,1.); -#259 = DIRECTION('',(-0.,1.,0.)); -#260 = ORIENTED_EDGE('',*,*,#261,.T.); -#261 = EDGE_CURVE('',#254,#262,#264,.T.); -#262 = VERTEX_POINT('',#263); -#263 = CARTESIAN_POINT('',(100.,20.,2.)); -#264 = LINE('',#265,#266); -#265 = CARTESIAN_POINT('',(100.,20.,0.)); -#266 = VECTOR('',#267,1.); -#267 = DIRECTION('',(0.,0.,1.)); -#268 = ORIENTED_EDGE('',*,*,#269,.F.); -#269 = EDGE_CURVE('',#246,#262,#270,.T.); -#270 = LINE('',#271,#272); -#271 = CARTESIAN_POINT('',(100.,-20.,2.)); -#272 = VECTOR('',#273,1.); -#273 = DIRECTION('',(-0.,1.,0.)); -#274 = PLANE('',#275); -#275 = AXIS2_PLACEMENT_3D('',#276,#277,#278); -#276 = CARTESIAN_POINT('',(100.,-20.,0.)); -#277 = DIRECTION('',(1.,0.,-0.)); -#278 = DIRECTION('',(0.,0.,1.)); -#279 = ADVANCED_FACE('',(#280),#296,.F.); -#280 = FACE_BOUND('',#281,.F.); -#281 = EDGE_LOOP('',(#282,#288,#289,#295)); -#282 = ORIENTED_EDGE('',*,*,#283,.F.); -#283 = EDGE_CURVE('',#204,#244,#284,.T.); -#284 = LINE('',#285,#286); -#285 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#286 = VECTOR('',#287,1.); -#287 = DIRECTION('',(1.,0.,-0.)); -#288 = ORIENTED_EDGE('',*,*,#203,.T.); -#289 = ORIENTED_EDGE('',*,*,#290,.T.); -#290 = EDGE_CURVE('',#206,#246,#291,.T.); -#291 = LINE('',#292,#293); -#292 = CARTESIAN_POINT('',(-100.,-20.,2.)); -#293 = VECTOR('',#294,1.); -#294 = DIRECTION('',(1.,0.,-0.)); -#295 = ORIENTED_EDGE('',*,*,#243,.F.); -#296 = PLANE('',#297); -#297 = AXIS2_PLACEMENT_3D('',#298,#299,#300); -#298 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#299 = DIRECTION('',(-0.,1.,0.)); -#300 = DIRECTION('',(0.,0.,1.)); -#301 = ADVANCED_FACE('',(#302),#318,.T.); -#302 = FACE_BOUND('',#303,.T.); -#303 = EDGE_LOOP('',(#304,#310,#311,#317)); -#304 = ORIENTED_EDGE('',*,*,#305,.F.); -#305 = EDGE_CURVE('',#214,#254,#306,.T.); -#306 = LINE('',#307,#308); -#307 = CARTESIAN_POINT('',(-100.,20.,0.)); -#308 = VECTOR('',#309,1.); -#309 = DIRECTION('',(1.,0.,-0.)); -#310 = ORIENTED_EDGE('',*,*,#221,.T.); -#311 = ORIENTED_EDGE('',*,*,#312,.T.); -#312 = EDGE_CURVE('',#222,#262,#313,.T.); -#313 = LINE('',#314,#315); -#314 = CARTESIAN_POINT('',(-100.,20.,2.)); -#315 = VECTOR('',#316,1.); -#316 = DIRECTION('',(1.,0.,-0.)); -#317 = ORIENTED_EDGE('',*,*,#261,.F.); -#318 = PLANE('',#319); -#319 = AXIS2_PLACEMENT_3D('',#320,#321,#322); -#320 = CARTESIAN_POINT('',(-100.,20.,0.)); -#321 = DIRECTION('',(-0.,1.,0.)); -#322 = DIRECTION('',(0.,0.,1.)); -#323 = ADVANCED_FACE('',(#324),#330,.F.); -#324 = FACE_BOUND('',#325,.F.); -#325 = EDGE_LOOP('',(#326,#327,#328,#329)); -#326 = ORIENTED_EDGE('',*,*,#213,.F.); -#327 = ORIENTED_EDGE('',*,*,#283,.T.); -#328 = ORIENTED_EDGE('',*,*,#253,.T.); -#329 = ORIENTED_EDGE('',*,*,#305,.F.); -#330 = PLANE('',#331); -#331 = AXIS2_PLACEMENT_3D('',#332,#333,#334); -#332 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#333 = DIRECTION('',(0.,0.,1.)); -#334 = DIRECTION('',(1.,0.,-0.)); -#335 = ADVANCED_FACE('',(#336),#342,.T.); -#336 = FACE_BOUND('',#337,.T.); -#337 = EDGE_LOOP('',(#338,#339,#340,#341)); -#338 = ORIENTED_EDGE('',*,*,#229,.F.); -#339 = ORIENTED_EDGE('',*,*,#290,.T.); -#340 = ORIENTED_EDGE('',*,*,#269,.T.); -#341 = ORIENTED_EDGE('',*,*,#312,.F.); -#342 = PLANE('',#343); -#343 = AXIS2_PLACEMENT_3D('',#344,#345,#346); -#344 = CARTESIAN_POINT('',(-100.,-20.,2.)); -#345 = DIRECTION('',(0.,0.,1.)); -#346 = DIRECTION('',(1.,0.,-0.)); -#347 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#351)) GLOBAL_UNIT_ASSIGNED_CONTEXT -((#348,#349,#350)) REPRESENTATION_CONTEXT('Context #1', - '3D Context with UNIT and UNCERTAINTY') ); -#348 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#349 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#350 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#351 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#348, - 'distance_accuracy_value','confusion accuracy'); -#352 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#353,#355); -#353 = ( REPRESENTATION_RELATIONSHIP('','',#196,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#354) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#354 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15); -#355 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#356 - ); -#356 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','WireDuct_BasePlate','',#5, - #191,$); -#357 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#193)); -#358 = SHAPE_DEFINITION_REPRESENTATION(#359,#365); -#359 = PRODUCT_DEFINITION_SHAPE('','',#360); -#360 = PRODUCT_DEFINITION('design','',#361,#364); -#361 = PRODUCT_DEFINITION_FORMATION('','',#362); -#362 = PRODUCT('WireDuct_LeftWall','WireDuct_LeftWall','',(#363)); -#363 = PRODUCT_CONTEXT('',#2,'mechanical'); -#364 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#365 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#366),#516); -#366 = MANIFOLD_SOLID_BREP('',#367); -#367 = CLOSED_SHELL('',(#368,#408,#448,#470,#492,#504)); -#368 = ADVANCED_FACE('',(#369),#403,.F.); -#369 = FACE_BOUND('',#370,.F.); -#370 = EDGE_LOOP('',(#371,#381,#389,#397)); -#371 = ORIENTED_EDGE('',*,*,#372,.F.); -#372 = EDGE_CURVE('',#373,#375,#377,.T.); +#150 = ADVANCED_FACE('',(#151),#169,.F.); +#151 = FACE_BOUND('',#152,.F.); +#152 = EDGE_LOOP('',(#153,#154,#155,#163)); +#153 = ORIENTED_EDGE('',*,*,#31,.F.); +#154 = ORIENTED_EDGE('',*,*,#61,.T.); +#155 = ORIENTED_EDGE('',*,*,#156,.T.); +#156 = EDGE_CURVE('',#62,#157,#159,.T.); +#157 = VERTEX_POINT('',#158); +#158 = CARTESIAN_POINT('',(100.,-18.,0.)); +#159 = LINE('',#160,#161); +#160 = CARTESIAN_POINT('',(100.,-20.,0.)); +#161 = VECTOR('',#162,1.); +#162 = DIRECTION('',(-0.,1.,0.)); +#163 = ORIENTED_EDGE('',*,*,#164,.F.); +#164 = EDGE_CURVE('',#32,#157,#165,.T.); +#165 = LINE('',#166,#167); +#166 = CARTESIAN_POINT('',(-100.,-18.,0.)); +#167 = VECTOR('',#168,1.); +#168 = DIRECTION('',(1.,0.,-0.)); +#169 = PLANE('',#170); +#170 = AXIS2_PLACEMENT_3D('',#171,#172,#173); +#171 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#172 = DIRECTION('',(0.,0.,1.)); +#173 = DIRECTION('',(1.,0.,-0.)); +#174 = ADVANCED_FACE('',(#175),#193,.T.); +#175 = FACE_BOUND('',#176,.T.); +#176 = EDGE_LOOP('',(#177,#178,#179,#187)); +#177 = ORIENTED_EDGE('',*,*,#78,.F.); +#178 = ORIENTED_EDGE('',*,*,#156,.T.); +#179 = ORIENTED_EDGE('',*,*,#180,.T.); +#180 = EDGE_CURVE('',#157,#181,#183,.T.); +#181 = VERTEX_POINT('',#182); +#182 = CARTESIAN_POINT('',(100.,-18.,2.)); +#183 = LINE('',#184,#185); +#184 = CARTESIAN_POINT('',(100.,-18.,0.)); +#185 = VECTOR('',#186,1.); +#186 = DIRECTION('',(0.,0.,1.)); +#187 = ORIENTED_EDGE('',*,*,#188,.F.); +#188 = EDGE_CURVE('',#71,#181,#189,.T.); +#189 = LINE('',#190,#191); +#190 = CARTESIAN_POINT('',(100.,-20.,2.)); +#191 = VECTOR('',#192,1.); +#192 = DIRECTION('',(-0.,1.,0.)); +#193 = PLANE('',#194); +#194 = AXIS2_PLACEMENT_3D('',#195,#196,#197); +#195 = CARTESIAN_POINT('',(100.,-20.,0.)); +#196 = DIRECTION('',(1.,0.,-0.)); +#197 = DIRECTION('',(0.,0.,1.)); +#198 = ADVANCED_FACE('',(#199),#793,.F.); +#199 = FACE_BOUND('',#200,.F.); +#200 = EDGE_LOOP('',(#201,#202,#203,#211,#219,#227,#235,#243,#251,#259, + #267,#275,#283,#291,#299,#307,#315,#323,#331,#339,#347,#355,#363, + #371,#379,#387,#395,#403,#411,#419,#427,#435,#443,#451,#459,#467, + #475,#483,#491,#499,#507,#515,#523,#531,#539,#547,#555,#563,#571, + #579,#587,#595,#603,#611,#619,#627,#635,#643,#651,#659,#667,#675, + #683,#691,#699,#707,#715,#723,#731,#739,#747,#755,#763,#771,#779, + #787)); +#201 = ORIENTED_EDGE('',*,*,#70,.F.); +#202 = ORIENTED_EDGE('',*,*,#92,.T.); +#203 = ORIENTED_EDGE('',*,*,#204,.T.); +#204 = EDGE_CURVE('',#93,#205,#207,.T.); +#205 = VERTEX_POINT('',#206); +#206 = CARTESIAN_POINT('',(-96.94444444444,-20.,40.)); +#207 = LINE('',#208,#209); +#208 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#209 = VECTOR('',#210,1.); +#210 = DIRECTION('',(1.,0.,-0.)); +#211 = ORIENTED_EDGE('',*,*,#212,.T.); +#212 = EDGE_CURVE('',#205,#213,#215,.T.); +#213 = VERTEX_POINT('',#214); +#214 = CARTESIAN_POINT('',(-96.94444444444,-20.,8.)); +#215 = LINE('',#216,#217); +#216 = CARTESIAN_POINT('',(-96.94444444444,-20.,4.)); +#217 = VECTOR('',#218,1.); +#218 = DIRECTION('',(-0.,0.,-1.)); +#219 = ORIENTED_EDGE('',*,*,#220,.T.); +#220 = EDGE_CURVE('',#213,#221,#223,.T.); +#221 = VERTEX_POINT('',#222); +#222 = CARTESIAN_POINT('',(-91.94444444444,-20.,8.)); +#223 = LINE('',#224,#225); +#224 = CARTESIAN_POINT('',(-98.47222222222,-20.,8.)); +#225 = VECTOR('',#226,1.); +#226 = DIRECTION('',(1.,0.,-0.)); +#227 = ORIENTED_EDGE('',*,*,#228,.F.); +#228 = EDGE_CURVE('',#229,#221,#231,.T.); +#229 = VERTEX_POINT('',#230); +#230 = CARTESIAN_POINT('',(-91.94444444444,-20.,40.)); +#231 = LINE('',#232,#233); +#232 = CARTESIAN_POINT('',(-91.94444444444,-20.,4.)); +#233 = VECTOR('',#234,1.); +#234 = DIRECTION('',(-0.,0.,-1.)); +#235 = ORIENTED_EDGE('',*,*,#236,.T.); +#236 = EDGE_CURVE('',#229,#237,#239,.T.); +#237 = VERTEX_POINT('',#238); +#238 = CARTESIAN_POINT('',(-85.83333333333,-20.,40.)); +#239 = LINE('',#240,#241); +#240 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#241 = VECTOR('',#242,1.); +#242 = DIRECTION('',(1.,0.,-0.)); +#243 = ORIENTED_EDGE('',*,*,#244,.T.); +#244 = EDGE_CURVE('',#237,#245,#247,.T.); +#245 = VERTEX_POINT('',#246); +#246 = CARTESIAN_POINT('',(-85.83333333333,-20.,8.)); +#247 = LINE('',#248,#249); +#248 = CARTESIAN_POINT('',(-85.83333333333,-20.,4.)); +#249 = VECTOR('',#250,1.); +#250 = DIRECTION('',(-0.,0.,-1.)); +#251 = ORIENTED_EDGE('',*,*,#252,.T.); +#252 = EDGE_CURVE('',#245,#253,#255,.T.); +#253 = VERTEX_POINT('',#254); +#254 = CARTESIAN_POINT('',(-80.83333333333,-20.,8.)); +#255 = LINE('',#256,#257); +#256 = CARTESIAN_POINT('',(-92.91666666666,-20.,8.)); +#257 = VECTOR('',#258,1.); +#258 = DIRECTION('',(1.,0.,-0.)); +#259 = ORIENTED_EDGE('',*,*,#260,.F.); +#260 = EDGE_CURVE('',#261,#253,#263,.T.); +#261 = VERTEX_POINT('',#262); +#262 = CARTESIAN_POINT('',(-80.83333333333,-20.,40.)); +#263 = LINE('',#264,#265); +#264 = CARTESIAN_POINT('',(-80.83333333333,-20.,4.)); +#265 = VECTOR('',#266,1.); +#266 = DIRECTION('',(-0.,0.,-1.)); +#267 = ORIENTED_EDGE('',*,*,#268,.T.); +#268 = EDGE_CURVE('',#261,#269,#271,.T.); +#269 = VERTEX_POINT('',#270); +#270 = CARTESIAN_POINT('',(-74.72222222222,-20.,40.)); +#271 = LINE('',#272,#273); +#272 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#273 = VECTOR('',#274,1.); +#274 = DIRECTION('',(1.,0.,-0.)); +#275 = ORIENTED_EDGE('',*,*,#276,.T.); +#276 = EDGE_CURVE('',#269,#277,#279,.T.); +#277 = VERTEX_POINT('',#278); +#278 = CARTESIAN_POINT('',(-74.72222222222,-20.,8.)); +#279 = LINE('',#280,#281); +#280 = CARTESIAN_POINT('',(-74.72222222222,-20.,4.)); +#281 = VECTOR('',#282,1.); +#282 = DIRECTION('',(-0.,0.,-1.)); +#283 = ORIENTED_EDGE('',*,*,#284,.T.); +#284 = EDGE_CURVE('',#277,#285,#287,.T.); +#285 = VERTEX_POINT('',#286); +#286 = CARTESIAN_POINT('',(-69.72222222222,-20.,8.)); +#287 = LINE('',#288,#289); +#288 = CARTESIAN_POINT('',(-87.36111111111,-20.,8.)); +#289 = VECTOR('',#290,1.); +#290 = DIRECTION('',(1.,0.,-0.)); +#291 = ORIENTED_EDGE('',*,*,#292,.F.); +#292 = EDGE_CURVE('',#293,#285,#295,.T.); +#293 = VERTEX_POINT('',#294); +#294 = CARTESIAN_POINT('',(-69.72222222222,-20.,40.)); +#295 = LINE('',#296,#297); +#296 = CARTESIAN_POINT('',(-69.72222222222,-20.,4.)); +#297 = VECTOR('',#298,1.); +#298 = DIRECTION('',(-0.,0.,-1.)); +#299 = ORIENTED_EDGE('',*,*,#300,.T.); +#300 = EDGE_CURVE('',#293,#301,#303,.T.); +#301 = VERTEX_POINT('',#302); +#302 = CARTESIAN_POINT('',(-63.61111111111,-20.,40.)); +#303 = LINE('',#304,#305); +#304 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#305 = VECTOR('',#306,1.); +#306 = DIRECTION('',(1.,0.,-0.)); +#307 = ORIENTED_EDGE('',*,*,#308,.T.); +#308 = EDGE_CURVE('',#301,#309,#311,.T.); +#309 = VERTEX_POINT('',#310); +#310 = CARTESIAN_POINT('',(-63.61111111111,-20.,8.)); +#311 = LINE('',#312,#313); +#312 = CARTESIAN_POINT('',(-63.61111111111,-20.,4.)); +#313 = VECTOR('',#314,1.); +#314 = DIRECTION('',(-0.,0.,-1.)); +#315 = ORIENTED_EDGE('',*,*,#316,.T.); +#316 = EDGE_CURVE('',#309,#317,#319,.T.); +#317 = VERTEX_POINT('',#318); +#318 = CARTESIAN_POINT('',(-58.61111111111,-20.,8.)); +#319 = LINE('',#320,#321); +#320 = CARTESIAN_POINT('',(-81.80555555555,-20.,8.)); +#321 = VECTOR('',#322,1.); +#322 = DIRECTION('',(1.,0.,-0.)); +#323 = ORIENTED_EDGE('',*,*,#324,.F.); +#324 = EDGE_CURVE('',#325,#317,#327,.T.); +#325 = VERTEX_POINT('',#326); +#326 = CARTESIAN_POINT('',(-58.61111111111,-20.,40.)); +#327 = LINE('',#328,#329); +#328 = CARTESIAN_POINT('',(-58.61111111111,-20.,4.)); +#329 = VECTOR('',#330,1.); +#330 = DIRECTION('',(-0.,0.,-1.)); +#331 = ORIENTED_EDGE('',*,*,#332,.T.); +#332 = EDGE_CURVE('',#325,#333,#335,.T.); +#333 = VERTEX_POINT('',#334); +#334 = CARTESIAN_POINT('',(-52.5,-20.,40.)); +#335 = LINE('',#336,#337); +#336 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#337 = VECTOR('',#338,1.); +#338 = DIRECTION('',(1.,0.,-0.)); +#339 = ORIENTED_EDGE('',*,*,#340,.T.); +#340 = EDGE_CURVE('',#333,#341,#343,.T.); +#341 = VERTEX_POINT('',#342); +#342 = CARTESIAN_POINT('',(-52.5,-20.,8.)); +#343 = LINE('',#344,#345); +#344 = CARTESIAN_POINT('',(-52.5,-20.,4.)); +#345 = VECTOR('',#346,1.); +#346 = DIRECTION('',(-0.,0.,-1.)); +#347 = ORIENTED_EDGE('',*,*,#348,.T.); +#348 = EDGE_CURVE('',#341,#349,#351,.T.); +#349 = VERTEX_POINT('',#350); +#350 = CARTESIAN_POINT('',(-47.5,-20.,8.)); +#351 = LINE('',#352,#353); +#352 = CARTESIAN_POINT('',(-76.25,-20.,8.)); +#353 = VECTOR('',#354,1.); +#354 = DIRECTION('',(1.,0.,-0.)); +#355 = ORIENTED_EDGE('',*,*,#356,.F.); +#356 = EDGE_CURVE('',#357,#349,#359,.T.); +#357 = VERTEX_POINT('',#358); +#358 = CARTESIAN_POINT('',(-47.5,-20.,40.)); +#359 = LINE('',#360,#361); +#360 = CARTESIAN_POINT('',(-47.5,-20.,4.)); +#361 = VECTOR('',#362,1.); +#362 = DIRECTION('',(-0.,0.,-1.)); +#363 = ORIENTED_EDGE('',*,*,#364,.T.); +#364 = EDGE_CURVE('',#357,#365,#367,.T.); +#365 = VERTEX_POINT('',#366); +#366 = CARTESIAN_POINT('',(-41.38888888888,-20.,40.)); +#367 = LINE('',#368,#369); +#368 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#369 = VECTOR('',#370,1.); +#370 = DIRECTION('',(1.,0.,-0.)); +#371 = ORIENTED_EDGE('',*,*,#372,.T.); +#372 = EDGE_CURVE('',#365,#373,#375,.T.); #373 = VERTEX_POINT('',#374); -#374 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#375 = VERTEX_POINT('',#376); -#376 = CARTESIAN_POINT('',(-100.,-20.,40.)); -#377 = LINE('',#378,#379); -#378 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#379 = VECTOR('',#380,1.); -#380 = DIRECTION('',(0.,0.,1.)); -#381 = ORIENTED_EDGE('',*,*,#382,.T.); -#382 = EDGE_CURVE('',#373,#383,#385,.T.); -#383 = VERTEX_POINT('',#384); -#384 = CARTESIAN_POINT('',(-100.,-18.,0.)); -#385 = LINE('',#386,#387); -#386 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#387 = VECTOR('',#388,1.); -#388 = DIRECTION('',(-0.,1.,0.)); -#389 = ORIENTED_EDGE('',*,*,#390,.T.); -#390 = EDGE_CURVE('',#383,#391,#393,.T.); -#391 = VERTEX_POINT('',#392); -#392 = CARTESIAN_POINT('',(-100.,-18.,40.)); -#393 = LINE('',#394,#395); -#394 = CARTESIAN_POINT('',(-100.,-18.,0.)); -#395 = VECTOR('',#396,1.); -#396 = DIRECTION('',(0.,0.,1.)); -#397 = ORIENTED_EDGE('',*,*,#398,.F.); -#398 = EDGE_CURVE('',#375,#391,#399,.T.); +#374 = CARTESIAN_POINT('',(-41.38888888888,-20.,8.)); +#375 = LINE('',#376,#377); +#376 = CARTESIAN_POINT('',(-41.38888888888,-20.,4.)); +#377 = VECTOR('',#378,1.); +#378 = DIRECTION('',(-0.,0.,-1.)); +#379 = ORIENTED_EDGE('',*,*,#380,.T.); +#380 = EDGE_CURVE('',#373,#381,#383,.T.); +#381 = VERTEX_POINT('',#382); +#382 = CARTESIAN_POINT('',(-36.38888888888,-20.,8.)); +#383 = LINE('',#384,#385); +#384 = CARTESIAN_POINT('',(-70.69444444444,-20.,8.)); +#385 = VECTOR('',#386,1.); +#386 = DIRECTION('',(1.,0.,-0.)); +#387 = ORIENTED_EDGE('',*,*,#388,.F.); +#388 = EDGE_CURVE('',#389,#381,#391,.T.); +#389 = VERTEX_POINT('',#390); +#390 = CARTESIAN_POINT('',(-36.38888888888,-20.,40.)); +#391 = LINE('',#392,#393); +#392 = CARTESIAN_POINT('',(-36.38888888888,-20.,4.)); +#393 = VECTOR('',#394,1.); +#394 = DIRECTION('',(-0.,0.,-1.)); +#395 = ORIENTED_EDGE('',*,*,#396,.T.); +#396 = EDGE_CURVE('',#389,#397,#399,.T.); +#397 = VERTEX_POINT('',#398); +#398 = CARTESIAN_POINT('',(-30.27777777777,-20.,40.)); #399 = LINE('',#400,#401); #400 = CARTESIAN_POINT('',(-100.,-20.,40.)); #401 = VECTOR('',#402,1.); -#402 = DIRECTION('',(-0.,1.,0.)); -#403 = PLANE('',#404); -#404 = AXIS2_PLACEMENT_3D('',#405,#406,#407); -#405 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#406 = DIRECTION('',(1.,0.,-0.)); -#407 = DIRECTION('',(0.,0.,1.)); -#408 = ADVANCED_FACE('',(#409),#443,.T.); -#409 = FACE_BOUND('',#410,.T.); -#410 = EDGE_LOOP('',(#411,#421,#429,#437)); -#411 = ORIENTED_EDGE('',*,*,#412,.F.); -#412 = EDGE_CURVE('',#413,#415,#417,.T.); +#402 = DIRECTION('',(1.,0.,-0.)); +#403 = ORIENTED_EDGE('',*,*,#404,.T.); +#404 = EDGE_CURVE('',#397,#405,#407,.T.); +#405 = VERTEX_POINT('',#406); +#406 = CARTESIAN_POINT('',(-30.27777777777,-20.,8.)); +#407 = LINE('',#408,#409); +#408 = CARTESIAN_POINT('',(-30.27777777777,-20.,4.)); +#409 = VECTOR('',#410,1.); +#410 = DIRECTION('',(-0.,0.,-1.)); +#411 = ORIENTED_EDGE('',*,*,#412,.T.); +#412 = EDGE_CURVE('',#405,#413,#415,.T.); #413 = VERTEX_POINT('',#414); -#414 = CARTESIAN_POINT('',(100.,-20.,0.)); -#415 = VERTEX_POINT('',#416); -#416 = CARTESIAN_POINT('',(100.,-20.,40.)); -#417 = LINE('',#418,#419); -#418 = CARTESIAN_POINT('',(100.,-20.,0.)); -#419 = VECTOR('',#420,1.); -#420 = DIRECTION('',(0.,0.,1.)); -#421 = ORIENTED_EDGE('',*,*,#422,.T.); -#422 = EDGE_CURVE('',#413,#423,#425,.T.); -#423 = VERTEX_POINT('',#424); -#424 = CARTESIAN_POINT('',(100.,-18.,0.)); -#425 = LINE('',#426,#427); -#426 = CARTESIAN_POINT('',(100.,-20.,0.)); -#427 = VECTOR('',#428,1.); -#428 = DIRECTION('',(-0.,1.,0.)); -#429 = ORIENTED_EDGE('',*,*,#430,.T.); -#430 = EDGE_CURVE('',#423,#431,#433,.T.); -#431 = VERTEX_POINT('',#432); -#432 = CARTESIAN_POINT('',(100.,-18.,40.)); -#433 = LINE('',#434,#435); -#434 = CARTESIAN_POINT('',(100.,-18.,0.)); -#435 = VECTOR('',#436,1.); -#436 = DIRECTION('',(0.,0.,1.)); -#437 = ORIENTED_EDGE('',*,*,#438,.F.); -#438 = EDGE_CURVE('',#415,#431,#439,.T.); +#414 = CARTESIAN_POINT('',(-25.27777777777,-20.,8.)); +#415 = LINE('',#416,#417); +#416 = CARTESIAN_POINT('',(-65.13888888888,-20.,8.)); +#417 = VECTOR('',#418,1.); +#418 = DIRECTION('',(1.,0.,-0.)); +#419 = ORIENTED_EDGE('',*,*,#420,.F.); +#420 = EDGE_CURVE('',#421,#413,#423,.T.); +#421 = VERTEX_POINT('',#422); +#422 = CARTESIAN_POINT('',(-25.27777777777,-20.,40.)); +#423 = LINE('',#424,#425); +#424 = CARTESIAN_POINT('',(-25.27777777777,-20.,4.)); +#425 = VECTOR('',#426,1.); +#426 = DIRECTION('',(-0.,0.,-1.)); +#427 = ORIENTED_EDGE('',*,*,#428,.T.); +#428 = EDGE_CURVE('',#421,#429,#431,.T.); +#429 = VERTEX_POINT('',#430); +#430 = CARTESIAN_POINT('',(-19.16666666666,-20.,40.)); +#431 = LINE('',#432,#433); +#432 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#433 = VECTOR('',#434,1.); +#434 = DIRECTION('',(1.,0.,-0.)); +#435 = ORIENTED_EDGE('',*,*,#436,.T.); +#436 = EDGE_CURVE('',#429,#437,#439,.T.); +#437 = VERTEX_POINT('',#438); +#438 = CARTESIAN_POINT('',(-19.16666666666,-20.,8.)); #439 = LINE('',#440,#441); -#440 = CARTESIAN_POINT('',(100.,-20.,40.)); +#440 = CARTESIAN_POINT('',(-19.16666666666,-20.,4.)); #441 = VECTOR('',#442,1.); -#442 = DIRECTION('',(-0.,1.,0.)); -#443 = PLANE('',#444); -#444 = AXIS2_PLACEMENT_3D('',#445,#446,#447); -#445 = CARTESIAN_POINT('',(100.,-20.,0.)); -#446 = DIRECTION('',(1.,0.,-0.)); -#447 = DIRECTION('',(0.,0.,1.)); -#448 = ADVANCED_FACE('',(#449),#465,.F.); -#449 = FACE_BOUND('',#450,.F.); -#450 = EDGE_LOOP('',(#451,#457,#458,#464)); +#442 = DIRECTION('',(-0.,0.,-1.)); +#443 = ORIENTED_EDGE('',*,*,#444,.T.); +#444 = EDGE_CURVE('',#437,#445,#447,.T.); +#445 = VERTEX_POINT('',#446); +#446 = CARTESIAN_POINT('',(-14.16666666666,-20.,8.)); +#447 = LINE('',#448,#449); +#448 = CARTESIAN_POINT('',(-59.58333333333,-20.,8.)); +#449 = VECTOR('',#450,1.); +#450 = DIRECTION('',(1.,0.,-0.)); #451 = ORIENTED_EDGE('',*,*,#452,.F.); -#452 = EDGE_CURVE('',#373,#413,#453,.T.); -#453 = LINE('',#454,#455); -#454 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#455 = VECTOR('',#456,1.); -#456 = DIRECTION('',(1.,0.,-0.)); -#457 = ORIENTED_EDGE('',*,*,#372,.T.); -#458 = ORIENTED_EDGE('',*,*,#459,.T.); -#459 = EDGE_CURVE('',#375,#415,#460,.T.); -#460 = LINE('',#461,#462); -#461 = CARTESIAN_POINT('',(-100.,-20.,40.)); -#462 = VECTOR('',#463,1.); -#463 = DIRECTION('',(1.,0.,-0.)); -#464 = ORIENTED_EDGE('',*,*,#412,.F.); -#465 = PLANE('',#466); -#466 = AXIS2_PLACEMENT_3D('',#467,#468,#469); -#467 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#468 = DIRECTION('',(-0.,1.,0.)); -#469 = DIRECTION('',(0.,0.,1.)); -#470 = ADVANCED_FACE('',(#471),#487,.T.); -#471 = FACE_BOUND('',#472,.T.); -#472 = EDGE_LOOP('',(#473,#479,#480,#486)); -#473 = ORIENTED_EDGE('',*,*,#474,.F.); -#474 = EDGE_CURVE('',#383,#423,#475,.T.); -#475 = LINE('',#476,#477); -#476 = CARTESIAN_POINT('',(-100.,-18.,0.)); -#477 = VECTOR('',#478,1.); -#478 = DIRECTION('',(1.,0.,-0.)); -#479 = ORIENTED_EDGE('',*,*,#390,.T.); -#480 = ORIENTED_EDGE('',*,*,#481,.T.); -#481 = EDGE_CURVE('',#391,#431,#482,.T.); -#482 = LINE('',#483,#484); -#483 = CARTESIAN_POINT('',(-100.,-18.,40.)); -#484 = VECTOR('',#485,1.); -#485 = DIRECTION('',(1.,0.,-0.)); -#486 = ORIENTED_EDGE('',*,*,#430,.F.); -#487 = PLANE('',#488); -#488 = AXIS2_PLACEMENT_3D('',#489,#490,#491); -#489 = CARTESIAN_POINT('',(-100.,-18.,0.)); -#490 = DIRECTION('',(-0.,1.,0.)); -#491 = DIRECTION('',(0.,0.,1.)); -#492 = ADVANCED_FACE('',(#493),#499,.F.); -#493 = FACE_BOUND('',#494,.F.); -#494 = EDGE_LOOP('',(#495,#496,#497,#498)); -#495 = ORIENTED_EDGE('',*,*,#382,.F.); -#496 = ORIENTED_EDGE('',*,*,#452,.T.); -#497 = ORIENTED_EDGE('',*,*,#422,.T.); -#498 = ORIENTED_EDGE('',*,*,#474,.F.); -#499 = PLANE('',#500); -#500 = AXIS2_PLACEMENT_3D('',#501,#502,#503); -#501 = CARTESIAN_POINT('',(-100.,-20.,0.)); -#502 = DIRECTION('',(0.,0.,1.)); -#503 = DIRECTION('',(1.,0.,-0.)); -#504 = ADVANCED_FACE('',(#505),#511,.T.); -#505 = FACE_BOUND('',#506,.T.); -#506 = EDGE_LOOP('',(#507,#508,#509,#510)); -#507 = ORIENTED_EDGE('',*,*,#398,.F.); -#508 = ORIENTED_EDGE('',*,*,#459,.T.); -#509 = ORIENTED_EDGE('',*,*,#438,.T.); -#510 = ORIENTED_EDGE('',*,*,#481,.F.); -#511 = PLANE('',#512); -#512 = AXIS2_PLACEMENT_3D('',#513,#514,#515); -#513 = CARTESIAN_POINT('',(-100.,-20.,40.)); -#514 = DIRECTION('',(0.,0.,1.)); -#515 = DIRECTION('',(1.,0.,-0.)); -#516 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#520)) GLOBAL_UNIT_ASSIGNED_CONTEXT -((#517,#518,#519)) REPRESENTATION_CONTEXT('Context #1', - '3D Context with UNIT and UNCERTAINTY') ); -#517 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#518 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#519 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#520 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#517, - 'distance_accuracy_value','confusion accuracy'); -#521 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#522,#524); -#522 = ( REPRESENTATION_RELATIONSHIP('','',#365,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#523) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#523 = ITEM_DEFINED_TRANSFORMATION('','',#11,#19); -#524 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#525 - ); -#525 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('2','WireDuct_LeftWall','',#5,#360 - ,$); -#526 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#362)); -#527 = SHAPE_DEFINITION_REPRESENTATION(#528,#534); -#528 = PRODUCT_DEFINITION_SHAPE('','',#529); -#529 = PRODUCT_DEFINITION('design','',#530,#533); -#530 = PRODUCT_DEFINITION_FORMATION('','',#531); -#531 = PRODUCT('WireDuct_RightWall','WireDuct_RightWall','',(#532)); -#532 = PRODUCT_CONTEXT('',#2,'mechanical'); -#533 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#534 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#535),#685); -#535 = MANIFOLD_SOLID_BREP('',#536); -#536 = CLOSED_SHELL('',(#537,#577,#617,#639,#661,#673)); -#537 = ADVANCED_FACE('',(#538),#572,.F.); -#538 = FACE_BOUND('',#539,.F.); -#539 = EDGE_LOOP('',(#540,#550,#558,#566)); -#540 = ORIENTED_EDGE('',*,*,#541,.F.); -#541 = EDGE_CURVE('',#542,#544,#546,.T.); -#542 = VERTEX_POINT('',#543); -#543 = CARTESIAN_POINT('',(-100.,18.,0.)); -#544 = VERTEX_POINT('',#545); -#545 = CARTESIAN_POINT('',(-100.,18.,40.)); -#546 = LINE('',#547,#548); -#547 = CARTESIAN_POINT('',(-100.,18.,0.)); -#548 = VECTOR('',#549,1.); -#549 = DIRECTION('',(0.,0.,1.)); -#550 = ORIENTED_EDGE('',*,*,#551,.T.); -#551 = EDGE_CURVE('',#542,#552,#554,.T.); -#552 = VERTEX_POINT('',#553); -#553 = CARTESIAN_POINT('',(-100.,20.,0.)); -#554 = LINE('',#555,#556); -#555 = CARTESIAN_POINT('',(-100.,18.,0.)); -#556 = VECTOR('',#557,1.); -#557 = DIRECTION('',(-0.,1.,0.)); -#558 = ORIENTED_EDGE('',*,*,#559,.T.); -#559 = EDGE_CURVE('',#552,#560,#562,.T.); -#560 = VERTEX_POINT('',#561); -#561 = CARTESIAN_POINT('',(-100.,20.,40.)); -#562 = LINE('',#563,#564); -#563 = CARTESIAN_POINT('',(-100.,20.,0.)); -#564 = VECTOR('',#565,1.); -#565 = DIRECTION('',(0.,0.,1.)); -#566 = ORIENTED_EDGE('',*,*,#567,.F.); -#567 = EDGE_CURVE('',#544,#560,#568,.T.); -#568 = LINE('',#569,#570); -#569 = CARTESIAN_POINT('',(-100.,18.,40.)); -#570 = VECTOR('',#571,1.); -#571 = DIRECTION('',(-0.,1.,0.)); -#572 = PLANE('',#573); -#573 = AXIS2_PLACEMENT_3D('',#574,#575,#576); -#574 = CARTESIAN_POINT('',(-100.,18.,0.)); -#575 = DIRECTION('',(1.,0.,-0.)); -#576 = DIRECTION('',(0.,0.,1.)); -#577 = ADVANCED_FACE('',(#578),#612,.T.); -#578 = FACE_BOUND('',#579,.T.); -#579 = EDGE_LOOP('',(#580,#590,#598,#606)); -#580 = ORIENTED_EDGE('',*,*,#581,.F.); -#581 = EDGE_CURVE('',#582,#584,#586,.T.); -#582 = VERTEX_POINT('',#583); -#583 = CARTESIAN_POINT('',(100.,18.,0.)); -#584 = VERTEX_POINT('',#585); -#585 = CARTESIAN_POINT('',(100.,18.,40.)); -#586 = LINE('',#587,#588); -#587 = CARTESIAN_POINT('',(100.,18.,0.)); -#588 = VECTOR('',#589,1.); -#589 = DIRECTION('',(0.,0.,1.)); -#590 = ORIENTED_EDGE('',*,*,#591,.T.); -#591 = EDGE_CURVE('',#582,#592,#594,.T.); -#592 = VERTEX_POINT('',#593); -#593 = CARTESIAN_POINT('',(100.,20.,0.)); -#594 = LINE('',#595,#596); -#595 = CARTESIAN_POINT('',(100.,18.,0.)); -#596 = VECTOR('',#597,1.); -#597 = DIRECTION('',(-0.,1.,0.)); -#598 = ORIENTED_EDGE('',*,*,#599,.T.); -#599 = EDGE_CURVE('',#592,#600,#602,.T.); -#600 = VERTEX_POINT('',#601); -#601 = CARTESIAN_POINT('',(100.,20.,40.)); -#602 = LINE('',#603,#604); -#603 = CARTESIAN_POINT('',(100.,20.,0.)); -#604 = VECTOR('',#605,1.); -#605 = DIRECTION('',(0.,0.,1.)); -#606 = ORIENTED_EDGE('',*,*,#607,.F.); -#607 = EDGE_CURVE('',#584,#600,#608,.T.); -#608 = LINE('',#609,#610); -#609 = CARTESIAN_POINT('',(100.,18.,40.)); -#610 = VECTOR('',#611,1.); -#611 = DIRECTION('',(-0.,1.,0.)); -#612 = PLANE('',#613); -#613 = AXIS2_PLACEMENT_3D('',#614,#615,#616); -#614 = CARTESIAN_POINT('',(100.,18.,0.)); -#615 = DIRECTION('',(1.,0.,-0.)); -#616 = DIRECTION('',(0.,0.,1.)); -#617 = ADVANCED_FACE('',(#618),#634,.F.); -#618 = FACE_BOUND('',#619,.F.); -#619 = EDGE_LOOP('',(#620,#626,#627,#633)); -#620 = ORIENTED_EDGE('',*,*,#621,.F.); -#621 = EDGE_CURVE('',#542,#582,#622,.T.); -#622 = LINE('',#623,#624); -#623 = CARTESIAN_POINT('',(-100.,18.,0.)); -#624 = VECTOR('',#625,1.); -#625 = DIRECTION('',(1.,0.,-0.)); -#626 = ORIENTED_EDGE('',*,*,#541,.T.); +#452 = EDGE_CURVE('',#453,#445,#455,.T.); +#453 = VERTEX_POINT('',#454); +#454 = CARTESIAN_POINT('',(-14.16666666666,-20.,40.)); +#455 = LINE('',#456,#457); +#456 = CARTESIAN_POINT('',(-14.16666666666,-20.,4.)); +#457 = VECTOR('',#458,1.); +#458 = DIRECTION('',(-0.,0.,-1.)); +#459 = ORIENTED_EDGE('',*,*,#460,.T.); +#460 = EDGE_CURVE('',#453,#461,#463,.T.); +#461 = VERTEX_POINT('',#462); +#462 = CARTESIAN_POINT('',(-8.055555555556,-20.,40.)); +#463 = LINE('',#464,#465); +#464 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#465 = VECTOR('',#466,1.); +#466 = DIRECTION('',(1.,0.,-0.)); +#467 = ORIENTED_EDGE('',*,*,#468,.T.); +#468 = EDGE_CURVE('',#461,#469,#471,.T.); +#469 = VERTEX_POINT('',#470); +#470 = CARTESIAN_POINT('',(-8.055555555556,-20.,8.)); +#471 = LINE('',#472,#473); +#472 = CARTESIAN_POINT('',(-8.055555555556,-20.,4.)); +#473 = VECTOR('',#474,1.); +#474 = DIRECTION('',(-0.,0.,-1.)); +#475 = ORIENTED_EDGE('',*,*,#476,.T.); +#476 = EDGE_CURVE('',#469,#477,#479,.T.); +#477 = VERTEX_POINT('',#478); +#478 = CARTESIAN_POINT('',(-3.055555555556,-20.,8.)); +#479 = LINE('',#480,#481); +#480 = CARTESIAN_POINT('',(-54.02777777777,-20.,8.)); +#481 = VECTOR('',#482,1.); +#482 = DIRECTION('',(1.,0.,-0.)); +#483 = ORIENTED_EDGE('',*,*,#484,.F.); +#484 = EDGE_CURVE('',#485,#477,#487,.T.); +#485 = VERTEX_POINT('',#486); +#486 = CARTESIAN_POINT('',(-3.055555555556,-20.,40.)); +#487 = LINE('',#488,#489); +#488 = CARTESIAN_POINT('',(-3.055555555556,-20.,4.)); +#489 = VECTOR('',#490,1.); +#490 = DIRECTION('',(-0.,0.,-1.)); +#491 = ORIENTED_EDGE('',*,*,#492,.T.); +#492 = EDGE_CURVE('',#485,#493,#495,.T.); +#493 = VERTEX_POINT('',#494); +#494 = CARTESIAN_POINT('',(3.055555555556,-20.,40.)); +#495 = LINE('',#496,#497); +#496 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#497 = VECTOR('',#498,1.); +#498 = DIRECTION('',(1.,0.,-0.)); +#499 = ORIENTED_EDGE('',*,*,#500,.T.); +#500 = EDGE_CURVE('',#493,#501,#503,.T.); +#501 = VERTEX_POINT('',#502); +#502 = CARTESIAN_POINT('',(3.055555555556,-20.,8.)); +#503 = LINE('',#504,#505); +#504 = CARTESIAN_POINT('',(3.055555555556,-20.,4.)); +#505 = VECTOR('',#506,1.); +#506 = DIRECTION('',(-0.,0.,-1.)); +#507 = ORIENTED_EDGE('',*,*,#508,.T.); +#508 = EDGE_CURVE('',#501,#509,#511,.T.); +#509 = VERTEX_POINT('',#510); +#510 = CARTESIAN_POINT('',(8.055555555556,-20.,8.)); +#511 = LINE('',#512,#513); +#512 = CARTESIAN_POINT('',(-48.47222222222,-20.,8.)); +#513 = VECTOR('',#514,1.); +#514 = DIRECTION('',(1.,0.,-0.)); +#515 = ORIENTED_EDGE('',*,*,#516,.F.); +#516 = EDGE_CURVE('',#517,#509,#519,.T.); +#517 = VERTEX_POINT('',#518); +#518 = CARTESIAN_POINT('',(8.055555555556,-20.,40.)); +#519 = LINE('',#520,#521); +#520 = CARTESIAN_POINT('',(8.055555555556,-20.,4.)); +#521 = VECTOR('',#522,1.); +#522 = DIRECTION('',(-0.,0.,-1.)); +#523 = ORIENTED_EDGE('',*,*,#524,.T.); +#524 = EDGE_CURVE('',#517,#525,#527,.T.); +#525 = VERTEX_POINT('',#526); +#526 = CARTESIAN_POINT('',(14.166666666667,-20.,40.)); +#527 = LINE('',#528,#529); +#528 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#529 = VECTOR('',#530,1.); +#530 = DIRECTION('',(1.,0.,-0.)); +#531 = ORIENTED_EDGE('',*,*,#532,.T.); +#532 = EDGE_CURVE('',#525,#533,#535,.T.); +#533 = VERTEX_POINT('',#534); +#534 = CARTESIAN_POINT('',(14.166666666667,-20.,8.)); +#535 = LINE('',#536,#537); +#536 = CARTESIAN_POINT('',(14.166666666667,-20.,4.)); +#537 = VECTOR('',#538,1.); +#538 = DIRECTION('',(-0.,0.,-1.)); +#539 = ORIENTED_EDGE('',*,*,#540,.T.); +#540 = EDGE_CURVE('',#533,#541,#543,.T.); +#541 = VERTEX_POINT('',#542); +#542 = CARTESIAN_POINT('',(19.166666666667,-20.,8.)); +#543 = LINE('',#544,#545); +#544 = CARTESIAN_POINT('',(-42.91666666666,-20.,8.)); +#545 = VECTOR('',#546,1.); +#546 = DIRECTION('',(1.,0.,-0.)); +#547 = ORIENTED_EDGE('',*,*,#548,.F.); +#548 = EDGE_CURVE('',#549,#541,#551,.T.); +#549 = VERTEX_POINT('',#550); +#550 = CARTESIAN_POINT('',(19.166666666667,-20.,40.)); +#551 = LINE('',#552,#553); +#552 = CARTESIAN_POINT('',(19.166666666667,-20.,4.)); +#553 = VECTOR('',#554,1.); +#554 = DIRECTION('',(-0.,0.,-1.)); +#555 = ORIENTED_EDGE('',*,*,#556,.T.); +#556 = EDGE_CURVE('',#549,#557,#559,.T.); +#557 = VERTEX_POINT('',#558); +#558 = CARTESIAN_POINT('',(25.277777777778,-20.,40.)); +#559 = LINE('',#560,#561); +#560 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#561 = VECTOR('',#562,1.); +#562 = DIRECTION('',(1.,0.,-0.)); +#563 = ORIENTED_EDGE('',*,*,#564,.T.); +#564 = EDGE_CURVE('',#557,#565,#567,.T.); +#565 = VERTEX_POINT('',#566); +#566 = CARTESIAN_POINT('',(25.277777777778,-20.,8.)); +#567 = LINE('',#568,#569); +#568 = CARTESIAN_POINT('',(25.277777777778,-20.,4.)); +#569 = VECTOR('',#570,1.); +#570 = DIRECTION('',(-0.,0.,-1.)); +#571 = ORIENTED_EDGE('',*,*,#572,.T.); +#572 = EDGE_CURVE('',#565,#573,#575,.T.); +#573 = VERTEX_POINT('',#574); +#574 = CARTESIAN_POINT('',(30.277777777778,-20.,8.)); +#575 = LINE('',#576,#577); +#576 = CARTESIAN_POINT('',(-37.36111111111,-20.,8.)); +#577 = VECTOR('',#578,1.); +#578 = DIRECTION('',(1.,0.,-0.)); +#579 = ORIENTED_EDGE('',*,*,#580,.F.); +#580 = EDGE_CURVE('',#581,#573,#583,.T.); +#581 = VERTEX_POINT('',#582); +#582 = CARTESIAN_POINT('',(30.277777777778,-20.,40.)); +#583 = LINE('',#584,#585); +#584 = CARTESIAN_POINT('',(30.277777777778,-20.,4.)); +#585 = VECTOR('',#586,1.); +#586 = DIRECTION('',(-0.,0.,-1.)); +#587 = ORIENTED_EDGE('',*,*,#588,.T.); +#588 = EDGE_CURVE('',#581,#589,#591,.T.); +#589 = VERTEX_POINT('',#590); +#590 = CARTESIAN_POINT('',(36.388888888889,-20.,40.)); +#591 = LINE('',#592,#593); +#592 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#593 = VECTOR('',#594,1.); +#594 = DIRECTION('',(1.,0.,-0.)); +#595 = ORIENTED_EDGE('',*,*,#596,.T.); +#596 = EDGE_CURVE('',#589,#597,#599,.T.); +#597 = VERTEX_POINT('',#598); +#598 = CARTESIAN_POINT('',(36.388888888889,-20.,8.)); +#599 = LINE('',#600,#601); +#600 = CARTESIAN_POINT('',(36.388888888889,-20.,4.)); +#601 = VECTOR('',#602,1.); +#602 = DIRECTION('',(-0.,0.,-1.)); +#603 = ORIENTED_EDGE('',*,*,#604,.T.); +#604 = EDGE_CURVE('',#597,#605,#607,.T.); +#605 = VERTEX_POINT('',#606); +#606 = CARTESIAN_POINT('',(41.388888888889,-20.,8.)); +#607 = LINE('',#608,#609); +#608 = CARTESIAN_POINT('',(-31.80555555555,-20.,8.)); +#609 = VECTOR('',#610,1.); +#610 = DIRECTION('',(1.,0.,-0.)); +#611 = ORIENTED_EDGE('',*,*,#612,.F.); +#612 = EDGE_CURVE('',#613,#605,#615,.T.); +#613 = VERTEX_POINT('',#614); +#614 = CARTESIAN_POINT('',(41.388888888889,-20.,40.)); +#615 = LINE('',#616,#617); +#616 = CARTESIAN_POINT('',(41.388888888889,-20.,4.)); +#617 = VECTOR('',#618,1.); +#618 = DIRECTION('',(-0.,0.,-1.)); +#619 = ORIENTED_EDGE('',*,*,#620,.T.); +#620 = EDGE_CURVE('',#613,#621,#623,.T.); +#621 = VERTEX_POINT('',#622); +#622 = CARTESIAN_POINT('',(47.5,-20.,40.)); +#623 = LINE('',#624,#625); +#624 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#625 = VECTOR('',#626,1.); +#626 = DIRECTION('',(1.,0.,-0.)); #627 = ORIENTED_EDGE('',*,*,#628,.T.); -#628 = EDGE_CURVE('',#544,#584,#629,.T.); -#629 = LINE('',#630,#631); -#630 = CARTESIAN_POINT('',(-100.,18.,40.)); -#631 = VECTOR('',#632,1.); -#632 = DIRECTION('',(1.,0.,-0.)); -#633 = ORIENTED_EDGE('',*,*,#581,.F.); -#634 = PLANE('',#635); -#635 = AXIS2_PLACEMENT_3D('',#636,#637,#638); -#636 = CARTESIAN_POINT('',(-100.,18.,0.)); -#637 = DIRECTION('',(-0.,1.,0.)); -#638 = DIRECTION('',(0.,0.,1.)); -#639 = ADVANCED_FACE('',(#640),#656,.T.); -#640 = FACE_BOUND('',#641,.T.); -#641 = EDGE_LOOP('',(#642,#648,#649,#655)); -#642 = ORIENTED_EDGE('',*,*,#643,.F.); -#643 = EDGE_CURVE('',#552,#592,#644,.T.); -#644 = LINE('',#645,#646); -#645 = CARTESIAN_POINT('',(-100.,20.,0.)); -#646 = VECTOR('',#647,1.); -#647 = DIRECTION('',(1.,0.,-0.)); -#648 = ORIENTED_EDGE('',*,*,#559,.T.); -#649 = ORIENTED_EDGE('',*,*,#650,.T.); -#650 = EDGE_CURVE('',#560,#600,#651,.T.); -#651 = LINE('',#652,#653); -#652 = CARTESIAN_POINT('',(-100.,20.,40.)); -#653 = VECTOR('',#654,1.); -#654 = DIRECTION('',(1.,0.,-0.)); -#655 = ORIENTED_EDGE('',*,*,#599,.F.); -#656 = PLANE('',#657); -#657 = AXIS2_PLACEMENT_3D('',#658,#659,#660); -#658 = CARTESIAN_POINT('',(-100.,20.,0.)); -#659 = DIRECTION('',(-0.,1.,0.)); -#660 = DIRECTION('',(0.,0.,1.)); -#661 = ADVANCED_FACE('',(#662),#668,.F.); -#662 = FACE_BOUND('',#663,.F.); -#663 = EDGE_LOOP('',(#664,#665,#666,#667)); -#664 = ORIENTED_EDGE('',*,*,#551,.F.); -#665 = ORIENTED_EDGE('',*,*,#621,.T.); -#666 = ORIENTED_EDGE('',*,*,#591,.T.); -#667 = ORIENTED_EDGE('',*,*,#643,.F.); -#668 = PLANE('',#669); -#669 = AXIS2_PLACEMENT_3D('',#670,#671,#672); -#670 = CARTESIAN_POINT('',(-100.,18.,0.)); -#671 = DIRECTION('',(0.,0.,1.)); -#672 = DIRECTION('',(1.,0.,-0.)); -#673 = ADVANCED_FACE('',(#674),#680,.T.); -#674 = FACE_BOUND('',#675,.T.); -#675 = EDGE_LOOP('',(#676,#677,#678,#679)); -#676 = ORIENTED_EDGE('',*,*,#567,.F.); -#677 = ORIENTED_EDGE('',*,*,#628,.T.); -#678 = ORIENTED_EDGE('',*,*,#607,.T.); -#679 = ORIENTED_EDGE('',*,*,#650,.F.); -#680 = PLANE('',#681); -#681 = AXIS2_PLACEMENT_3D('',#682,#683,#684); -#682 = CARTESIAN_POINT('',(-100.,18.,40.)); -#683 = DIRECTION('',(0.,0.,1.)); -#684 = DIRECTION('',(1.,0.,-0.)); -#685 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#689)) GLOBAL_UNIT_ASSIGNED_CONTEXT -((#686,#687,#688)) REPRESENTATION_CONTEXT('Context #1', - '3D Context with UNIT and UNCERTAINTY') ); -#686 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#687 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#688 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#689 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#686, - 'distance_accuracy_value','confusion accuracy'); -#690 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#691,#693); -#691 = ( REPRESENTATION_RELATIONSHIP('','',#534,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#692) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#692 = ITEM_DEFINED_TRANSFORMATION('','',#11,#23); -#693 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#694 - ); -#694 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('3','WireDuct_RightWall','',#5, - #529,$); -#695 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#531)); -#696 = SHAPE_DEFINITION_REPRESENTATION(#697,#703); -#697 = PRODUCT_DEFINITION_SHAPE('','',#698); -#698 = PRODUCT_DEFINITION('design','',#699,#702); -#699 = PRODUCT_DEFINITION_FORMATION('','',#700); -#700 = PRODUCT('WireDuct_LeftCombSlot_01','WireDuct_LeftCombSlot_01','', - (#701)); -#701 = PRODUCT_CONTEXT('',#2,'mechanical'); -#702 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#703 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#704),#854); -#704 = MANIFOLD_SOLID_BREP('',#705); -#705 = CLOSED_SHELL('',(#706,#746,#786,#808,#830,#842)); -#706 = ADVANCED_FACE('',(#707),#741,.F.); -#707 = FACE_BOUND('',#708,.F.); -#708 = EDGE_LOOP('',(#709,#719,#727,#735)); -#709 = ORIENTED_EDGE('',*,*,#710,.F.); -#710 = EDGE_CURVE('',#711,#713,#715,.T.); -#711 = VERTEX_POINT('',#712); -#712 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#713 = VERTEX_POINT('',#714); -#714 = CARTESIAN_POINT('',(-96.94444444444,-20.1,40.)); -#715 = LINE('',#716,#717); -#716 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#717 = VECTOR('',#718,1.); -#718 = DIRECTION('',(0.,0.,1.)); -#719 = ORIENTED_EDGE('',*,*,#720,.T.); -#720 = EDGE_CURVE('',#711,#721,#723,.T.); -#721 = VERTEX_POINT('',#722); -#722 = CARTESIAN_POINT('',(-96.94444444444,-17.9,8.)); -#723 = LINE('',#724,#725); -#724 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#725 = VECTOR('',#726,1.); -#726 = DIRECTION('',(-0.,1.,0.)); -#727 = ORIENTED_EDGE('',*,*,#728,.T.); -#728 = EDGE_CURVE('',#721,#729,#731,.T.); -#729 = VERTEX_POINT('',#730); -#730 = CARTESIAN_POINT('',(-96.94444444444,-17.9,40.)); -#731 = LINE('',#732,#733); -#732 = CARTESIAN_POINT('',(-96.94444444444,-17.9,8.)); -#733 = VECTOR('',#734,1.); -#734 = DIRECTION('',(0.,0.,1.)); -#735 = ORIENTED_EDGE('',*,*,#736,.F.); -#736 = EDGE_CURVE('',#713,#729,#737,.T.); -#737 = LINE('',#738,#739); -#738 = CARTESIAN_POINT('',(-96.94444444444,-20.1,40.)); -#739 = VECTOR('',#740,1.); -#740 = DIRECTION('',(-0.,1.,0.)); -#741 = PLANE('',#742); -#742 = AXIS2_PLACEMENT_3D('',#743,#744,#745); -#743 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#744 = DIRECTION('',(1.,0.,-0.)); -#745 = DIRECTION('',(0.,0.,1.)); -#746 = ADVANCED_FACE('',(#747),#781,.T.); -#747 = FACE_BOUND('',#748,.T.); -#748 = EDGE_LOOP('',(#749,#759,#767,#775)); -#749 = ORIENTED_EDGE('',*,*,#750,.F.); -#750 = EDGE_CURVE('',#751,#753,#755,.T.); -#751 = VERTEX_POINT('',#752); -#752 = CARTESIAN_POINT('',(-91.94444444444,-20.1,8.)); -#753 = VERTEX_POINT('',#754); -#754 = CARTESIAN_POINT('',(-91.94444444444,-20.1,40.)); -#755 = LINE('',#756,#757); -#756 = CARTESIAN_POINT('',(-91.94444444444,-20.1,8.)); -#757 = VECTOR('',#758,1.); -#758 = DIRECTION('',(0.,0.,1.)); -#759 = ORIENTED_EDGE('',*,*,#760,.T.); -#760 = EDGE_CURVE('',#751,#761,#763,.T.); -#761 = VERTEX_POINT('',#762); -#762 = CARTESIAN_POINT('',(-91.94444444444,-17.9,8.)); -#763 = LINE('',#764,#765); -#764 = CARTESIAN_POINT('',(-91.94444444444,-20.1,8.)); -#765 = VECTOR('',#766,1.); -#766 = DIRECTION('',(-0.,1.,0.)); -#767 = ORIENTED_EDGE('',*,*,#768,.T.); -#768 = EDGE_CURVE('',#761,#769,#771,.T.); -#769 = VERTEX_POINT('',#770); -#770 = CARTESIAN_POINT('',(-91.94444444444,-17.9,40.)); -#771 = LINE('',#772,#773); -#772 = CARTESIAN_POINT('',(-91.94444444444,-17.9,8.)); -#773 = VECTOR('',#774,1.); -#774 = DIRECTION('',(0.,0.,1.)); -#775 = ORIENTED_EDGE('',*,*,#776,.F.); -#776 = EDGE_CURVE('',#753,#769,#777,.T.); -#777 = LINE('',#778,#779); -#778 = CARTESIAN_POINT('',(-91.94444444444,-20.1,40.)); -#779 = VECTOR('',#780,1.); -#780 = DIRECTION('',(-0.,1.,0.)); -#781 = PLANE('',#782); -#782 = AXIS2_PLACEMENT_3D('',#783,#784,#785); -#783 = CARTESIAN_POINT('',(-91.94444444444,-20.1,8.)); -#784 = DIRECTION('',(1.,0.,-0.)); -#785 = DIRECTION('',(0.,0.,1.)); -#786 = ADVANCED_FACE('',(#787),#803,.F.); -#787 = FACE_BOUND('',#788,.F.); -#788 = EDGE_LOOP('',(#789,#795,#796,#802)); -#789 = ORIENTED_EDGE('',*,*,#790,.F.); -#790 = EDGE_CURVE('',#711,#751,#791,.T.); -#791 = LINE('',#792,#793); -#792 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#793 = VECTOR('',#794,1.); -#794 = DIRECTION('',(1.,0.,-0.)); -#795 = ORIENTED_EDGE('',*,*,#710,.T.); -#796 = ORIENTED_EDGE('',*,*,#797,.T.); -#797 = EDGE_CURVE('',#713,#753,#798,.T.); -#798 = LINE('',#799,#800); -#799 = CARTESIAN_POINT('',(-96.94444444444,-20.1,40.)); -#800 = VECTOR('',#801,1.); -#801 = DIRECTION('',(1.,0.,-0.)); -#802 = ORIENTED_EDGE('',*,*,#750,.F.); -#803 = PLANE('',#804); -#804 = AXIS2_PLACEMENT_3D('',#805,#806,#807); -#805 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#806 = DIRECTION('',(-0.,1.,0.)); -#807 = DIRECTION('',(0.,0.,1.)); -#808 = ADVANCED_FACE('',(#809),#825,.T.); -#809 = FACE_BOUND('',#810,.T.); -#810 = EDGE_LOOP('',(#811,#817,#818,#824)); -#811 = ORIENTED_EDGE('',*,*,#812,.F.); -#812 = EDGE_CURVE('',#721,#761,#813,.T.); +#628 = EDGE_CURVE('',#621,#629,#631,.T.); +#629 = VERTEX_POINT('',#630); +#630 = CARTESIAN_POINT('',(47.5,-20.,8.)); +#631 = LINE('',#632,#633); +#632 = CARTESIAN_POINT('',(47.5,-20.,4.)); +#633 = VECTOR('',#634,1.); +#634 = DIRECTION('',(-0.,0.,-1.)); +#635 = ORIENTED_EDGE('',*,*,#636,.T.); +#636 = EDGE_CURVE('',#629,#637,#639,.T.); +#637 = VERTEX_POINT('',#638); +#638 = CARTESIAN_POINT('',(52.5,-20.,8.)); +#639 = LINE('',#640,#641); +#640 = CARTESIAN_POINT('',(-26.25,-20.,8.)); +#641 = VECTOR('',#642,1.); +#642 = DIRECTION('',(1.,0.,-0.)); +#643 = ORIENTED_EDGE('',*,*,#644,.F.); +#644 = EDGE_CURVE('',#645,#637,#647,.T.); +#645 = VERTEX_POINT('',#646); +#646 = CARTESIAN_POINT('',(52.5,-20.,40.)); +#647 = LINE('',#648,#649); +#648 = CARTESIAN_POINT('',(52.5,-20.,4.)); +#649 = VECTOR('',#650,1.); +#650 = DIRECTION('',(-0.,0.,-1.)); +#651 = ORIENTED_EDGE('',*,*,#652,.T.); +#652 = EDGE_CURVE('',#645,#653,#655,.T.); +#653 = VERTEX_POINT('',#654); +#654 = CARTESIAN_POINT('',(58.611111111111,-20.,40.)); +#655 = LINE('',#656,#657); +#656 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#657 = VECTOR('',#658,1.); +#658 = DIRECTION('',(1.,0.,-0.)); +#659 = ORIENTED_EDGE('',*,*,#660,.T.); +#660 = EDGE_CURVE('',#653,#661,#663,.T.); +#661 = VERTEX_POINT('',#662); +#662 = CARTESIAN_POINT('',(58.611111111111,-20.,8.)); +#663 = LINE('',#664,#665); +#664 = CARTESIAN_POINT('',(58.611111111111,-20.,4.)); +#665 = VECTOR('',#666,1.); +#666 = DIRECTION('',(-0.,0.,-1.)); +#667 = ORIENTED_EDGE('',*,*,#668,.T.); +#668 = EDGE_CURVE('',#661,#669,#671,.T.); +#669 = VERTEX_POINT('',#670); +#670 = CARTESIAN_POINT('',(63.611111111111,-20.,8.)); +#671 = LINE('',#672,#673); +#672 = CARTESIAN_POINT('',(-20.69444444444,-20.,8.)); +#673 = VECTOR('',#674,1.); +#674 = DIRECTION('',(1.,0.,-0.)); +#675 = ORIENTED_EDGE('',*,*,#676,.F.); +#676 = EDGE_CURVE('',#677,#669,#679,.T.); +#677 = VERTEX_POINT('',#678); +#678 = CARTESIAN_POINT('',(63.611111111111,-20.,40.)); +#679 = LINE('',#680,#681); +#680 = CARTESIAN_POINT('',(63.611111111111,-20.,4.)); +#681 = VECTOR('',#682,1.); +#682 = DIRECTION('',(-0.,0.,-1.)); +#683 = ORIENTED_EDGE('',*,*,#684,.T.); +#684 = EDGE_CURVE('',#677,#685,#687,.T.); +#685 = VERTEX_POINT('',#686); +#686 = CARTESIAN_POINT('',(69.722222222222,-20.,40.)); +#687 = LINE('',#688,#689); +#688 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#689 = VECTOR('',#690,1.); +#690 = DIRECTION('',(1.,0.,-0.)); +#691 = ORIENTED_EDGE('',*,*,#692,.T.); +#692 = EDGE_CURVE('',#685,#693,#695,.T.); +#693 = VERTEX_POINT('',#694); +#694 = CARTESIAN_POINT('',(69.722222222222,-20.,8.)); +#695 = LINE('',#696,#697); +#696 = CARTESIAN_POINT('',(69.722222222222,-20.,4.)); +#697 = VECTOR('',#698,1.); +#698 = DIRECTION('',(-0.,0.,-1.)); +#699 = ORIENTED_EDGE('',*,*,#700,.T.); +#700 = EDGE_CURVE('',#693,#701,#703,.T.); +#701 = VERTEX_POINT('',#702); +#702 = CARTESIAN_POINT('',(74.722222222222,-20.,8.)); +#703 = LINE('',#704,#705); +#704 = CARTESIAN_POINT('',(-15.13888888888,-20.,8.)); +#705 = VECTOR('',#706,1.); +#706 = DIRECTION('',(1.,0.,-0.)); +#707 = ORIENTED_EDGE('',*,*,#708,.F.); +#708 = EDGE_CURVE('',#709,#701,#711,.T.); +#709 = VERTEX_POINT('',#710); +#710 = CARTESIAN_POINT('',(74.722222222222,-20.,40.)); +#711 = LINE('',#712,#713); +#712 = CARTESIAN_POINT('',(74.722222222222,-20.,4.)); +#713 = VECTOR('',#714,1.); +#714 = DIRECTION('',(-0.,0.,-1.)); +#715 = ORIENTED_EDGE('',*,*,#716,.T.); +#716 = EDGE_CURVE('',#709,#717,#719,.T.); +#717 = VERTEX_POINT('',#718); +#718 = CARTESIAN_POINT('',(80.833333333333,-20.,40.)); +#719 = LINE('',#720,#721); +#720 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#721 = VECTOR('',#722,1.); +#722 = DIRECTION('',(1.,0.,-0.)); +#723 = ORIENTED_EDGE('',*,*,#724,.T.); +#724 = EDGE_CURVE('',#717,#725,#727,.T.); +#725 = VERTEX_POINT('',#726); +#726 = CARTESIAN_POINT('',(80.833333333333,-20.,8.)); +#727 = LINE('',#728,#729); +#728 = CARTESIAN_POINT('',(80.833333333333,-20.,4.)); +#729 = VECTOR('',#730,1.); +#730 = DIRECTION('',(-0.,0.,-1.)); +#731 = ORIENTED_EDGE('',*,*,#732,.T.); +#732 = EDGE_CURVE('',#725,#733,#735,.T.); +#733 = VERTEX_POINT('',#734); +#734 = CARTESIAN_POINT('',(85.833333333333,-20.,8.)); +#735 = LINE('',#736,#737); +#736 = CARTESIAN_POINT('',(-9.583333333333,-20.,8.)); +#737 = VECTOR('',#738,1.); +#738 = DIRECTION('',(1.,0.,-0.)); +#739 = ORIENTED_EDGE('',*,*,#740,.F.); +#740 = EDGE_CURVE('',#741,#733,#743,.T.); +#741 = VERTEX_POINT('',#742); +#742 = CARTESIAN_POINT('',(85.833333333333,-20.,40.)); +#743 = LINE('',#744,#745); +#744 = CARTESIAN_POINT('',(85.833333333333,-20.,4.)); +#745 = VECTOR('',#746,1.); +#746 = DIRECTION('',(-0.,0.,-1.)); +#747 = ORIENTED_EDGE('',*,*,#748,.T.); +#748 = EDGE_CURVE('',#741,#749,#751,.T.); +#749 = VERTEX_POINT('',#750); +#750 = CARTESIAN_POINT('',(91.944444444444,-20.,40.)); +#751 = LINE('',#752,#753); +#752 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#753 = VECTOR('',#754,1.); +#754 = DIRECTION('',(1.,0.,-0.)); +#755 = ORIENTED_EDGE('',*,*,#756,.T.); +#756 = EDGE_CURVE('',#749,#757,#759,.T.); +#757 = VERTEX_POINT('',#758); +#758 = CARTESIAN_POINT('',(91.944444444444,-20.,8.)); +#759 = LINE('',#760,#761); +#760 = CARTESIAN_POINT('',(91.944444444444,-20.,4.)); +#761 = VECTOR('',#762,1.); +#762 = DIRECTION('',(-0.,0.,-1.)); +#763 = ORIENTED_EDGE('',*,*,#764,.T.); +#764 = EDGE_CURVE('',#757,#765,#767,.T.); +#765 = VERTEX_POINT('',#766); +#766 = CARTESIAN_POINT('',(96.944444444444,-20.,8.)); +#767 = LINE('',#768,#769); +#768 = CARTESIAN_POINT('',(-4.027777777778,-20.,8.)); +#769 = VECTOR('',#770,1.); +#770 = DIRECTION('',(1.,0.,-0.)); +#771 = ORIENTED_EDGE('',*,*,#772,.F.); +#772 = EDGE_CURVE('',#773,#765,#775,.T.); +#773 = VERTEX_POINT('',#774); +#774 = CARTESIAN_POINT('',(96.944444444444,-20.,40.)); +#775 = LINE('',#776,#777); +#776 = CARTESIAN_POINT('',(96.944444444444,-20.,4.)); +#777 = VECTOR('',#778,1.); +#778 = DIRECTION('',(-0.,0.,-1.)); +#779 = ORIENTED_EDGE('',*,*,#780,.T.); +#780 = EDGE_CURVE('',#773,#781,#783,.T.); +#781 = VERTEX_POINT('',#782); +#782 = CARTESIAN_POINT('',(100.,-20.,40.)); +#783 = LINE('',#784,#785); +#784 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#785 = VECTOR('',#786,1.); +#786 = DIRECTION('',(1.,0.,-0.)); +#787 = ORIENTED_EDGE('',*,*,#788,.F.); +#788 = EDGE_CURVE('',#71,#781,#789,.T.); +#789 = LINE('',#790,#791); +#790 = CARTESIAN_POINT('',(100.,-20.,0.)); +#791 = VECTOR('',#792,1.); +#792 = DIRECTION('',(0.,0.,1.)); +#793 = PLANE('',#794); +#794 = AXIS2_PLACEMENT_3D('',#795,#796,#797); +#795 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#796 = DIRECTION('',(-0.,1.,0.)); +#797 = DIRECTION('',(0.,0.,1.)); +#798 = ADVANCED_FACE('',(#799),#817,.T.); +#799 = FACE_BOUND('',#800,.T.); +#800 = EDGE_LOOP('',(#801,#809,#810,#811)); +#801 = ORIENTED_EDGE('',*,*,#802,.F.); +#802 = EDGE_CURVE('',#102,#803,#805,.T.); +#803 = VERTEX_POINT('',#804); +#804 = CARTESIAN_POINT('',(-96.94444444444,-18.,40.)); +#805 = LINE('',#806,#807); +#806 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#807 = VECTOR('',#808,1.); +#808 = DIRECTION('',(1.,0.,-0.)); +#809 = ORIENTED_EDGE('',*,*,#109,.F.); +#810 = ORIENTED_EDGE('',*,*,#204,.T.); +#811 = ORIENTED_EDGE('',*,*,#812,.T.); +#812 = EDGE_CURVE('',#205,#803,#813,.T.); #813 = LINE('',#814,#815); -#814 = CARTESIAN_POINT('',(-96.94444444444,-17.9,8.)); +#814 = CARTESIAN_POINT('',(-96.94444444444,-20.2,40.)); #815 = VECTOR('',#816,1.); -#816 = DIRECTION('',(1.,0.,-0.)); -#817 = ORIENTED_EDGE('',*,*,#728,.T.); -#818 = ORIENTED_EDGE('',*,*,#819,.T.); -#819 = EDGE_CURVE('',#729,#769,#820,.T.); -#820 = LINE('',#821,#822); -#821 = CARTESIAN_POINT('',(-96.94444444444,-17.9,40.)); -#822 = VECTOR('',#823,1.); -#823 = DIRECTION('',(1.,0.,-0.)); -#824 = ORIENTED_EDGE('',*,*,#768,.F.); -#825 = PLANE('',#826); -#826 = AXIS2_PLACEMENT_3D('',#827,#828,#829); -#827 = CARTESIAN_POINT('',(-96.94444444444,-17.9,8.)); -#828 = DIRECTION('',(-0.,1.,0.)); -#829 = DIRECTION('',(0.,0.,1.)); -#830 = ADVANCED_FACE('',(#831),#837,.F.); -#831 = FACE_BOUND('',#832,.F.); -#832 = EDGE_LOOP('',(#833,#834,#835,#836)); -#833 = ORIENTED_EDGE('',*,*,#720,.F.); -#834 = ORIENTED_EDGE('',*,*,#790,.T.); -#835 = ORIENTED_EDGE('',*,*,#760,.T.); -#836 = ORIENTED_EDGE('',*,*,#812,.F.); -#837 = PLANE('',#838); -#838 = AXIS2_PLACEMENT_3D('',#839,#840,#841); -#839 = CARTESIAN_POINT('',(-96.94444444444,-20.1,8.)); -#840 = DIRECTION('',(0.,0.,1.)); -#841 = DIRECTION('',(1.,0.,-0.)); -#842 = ADVANCED_FACE('',(#843),#849,.T.); -#843 = FACE_BOUND('',#844,.T.); -#844 = EDGE_LOOP('',(#845,#846,#847,#848)); -#845 = ORIENTED_EDGE('',*,*,#736,.F.); -#846 = ORIENTED_EDGE('',*,*,#797,.T.); -#847 = ORIENTED_EDGE('',*,*,#776,.T.); -#848 = ORIENTED_EDGE('',*,*,#819,.F.); -#849 = PLANE('',#850); -#850 = AXIS2_PLACEMENT_3D('',#851,#852,#853); -#851 = CARTESIAN_POINT('',(-96.94444444444,-20.1,40.)); -#852 = DIRECTION('',(0.,0.,1.)); -#853 = DIRECTION('',(1.,0.,-0.)); -#854 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#858)) GLOBAL_UNIT_ASSIGNED_CONTEXT -((#855,#856,#857)) REPRESENTATION_CONTEXT('Context #1', - '3D Context with UNIT and UNCERTAINTY') ); -#855 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#856 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#857 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#858 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#855, - 'distance_accuracy_value','confusion accuracy'); -#859 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#860,#862); -#860 = ( REPRESENTATION_RELATIONSHIP('','',#703,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#861) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#861 = ITEM_DEFINED_TRANSFORMATION('','',#11,#27); -#862 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#863 - ); -#863 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('4','WireDuct_LeftCombSlot_01','', - #5,#698,$); -#864 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#700)); -#865 = SHAPE_DEFINITION_REPRESENTATION(#866,#872); -#866 = PRODUCT_DEFINITION_SHAPE('','',#867); -#867 = PRODUCT_DEFINITION('design','',#868,#871); -#868 = PRODUCT_DEFINITION_FORMATION('','',#869); -#869 = PRODUCT('WireDuct_RightCombSlot_01','WireDuct_RightCombSlot_01', - '',(#870)); -#870 = PRODUCT_CONTEXT('',#2,'mechanical'); -#871 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#872 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#873),#1023); -#873 = MANIFOLD_SOLID_BREP('',#874); -#874 = CLOSED_SHELL('',(#875,#915,#955,#977,#999,#1011)); -#875 = ADVANCED_FACE('',(#876),#910,.F.); -#876 = FACE_BOUND('',#877,.F.); -#877 = EDGE_LOOP('',(#878,#888,#896,#904)); -#878 = ORIENTED_EDGE('',*,*,#879,.F.); -#879 = EDGE_CURVE('',#880,#882,#884,.T.); -#880 = VERTEX_POINT('',#881); -#881 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#882 = VERTEX_POINT('',#883); -#883 = CARTESIAN_POINT('',(-96.94444444444,17.9,40.)); -#884 = LINE('',#885,#886); -#885 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#886 = VECTOR('',#887,1.); -#887 = DIRECTION('',(0.,0.,1.)); -#888 = ORIENTED_EDGE('',*,*,#889,.T.); -#889 = EDGE_CURVE('',#880,#890,#892,.T.); -#890 = VERTEX_POINT('',#891); -#891 = CARTESIAN_POINT('',(-96.94444444444,20.1,8.)); -#892 = LINE('',#893,#894); -#893 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#894 = VECTOR('',#895,1.); -#895 = DIRECTION('',(-0.,1.,0.)); -#896 = ORIENTED_EDGE('',*,*,#897,.T.); -#897 = EDGE_CURVE('',#890,#898,#900,.T.); -#898 = VERTEX_POINT('',#899); -#899 = CARTESIAN_POINT('',(-96.94444444444,20.1,40.)); -#900 = LINE('',#901,#902); -#901 = CARTESIAN_POINT('',(-96.94444444444,20.1,8.)); -#902 = VECTOR('',#903,1.); -#903 = DIRECTION('',(0.,0.,1.)); -#904 = ORIENTED_EDGE('',*,*,#905,.F.); -#905 = EDGE_CURVE('',#882,#898,#906,.T.); -#906 = LINE('',#907,#908); -#907 = CARTESIAN_POINT('',(-96.94444444444,17.9,40.)); -#908 = VECTOR('',#909,1.); -#909 = DIRECTION('',(-0.,1.,0.)); -#910 = PLANE('',#911); -#911 = AXIS2_PLACEMENT_3D('',#912,#913,#914); -#912 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#913 = DIRECTION('',(1.,0.,-0.)); -#914 = DIRECTION('',(0.,0.,1.)); -#915 = ADVANCED_FACE('',(#916),#950,.T.); -#916 = FACE_BOUND('',#917,.T.); -#917 = EDGE_LOOP('',(#918,#928,#936,#944)); -#918 = ORIENTED_EDGE('',*,*,#919,.F.); -#919 = EDGE_CURVE('',#920,#922,#924,.T.); -#920 = VERTEX_POINT('',#921); -#921 = CARTESIAN_POINT('',(-91.94444444444,17.9,8.)); -#922 = VERTEX_POINT('',#923); -#923 = CARTESIAN_POINT('',(-91.94444444444,17.9,40.)); -#924 = LINE('',#925,#926); -#925 = CARTESIAN_POINT('',(-91.94444444444,17.9,8.)); -#926 = VECTOR('',#927,1.); -#927 = DIRECTION('',(0.,0.,1.)); -#928 = ORIENTED_EDGE('',*,*,#929,.T.); -#929 = EDGE_CURVE('',#920,#930,#932,.T.); -#930 = VERTEX_POINT('',#931); -#931 = CARTESIAN_POINT('',(-91.94444444444,20.1,8.)); -#932 = LINE('',#933,#934); -#933 = CARTESIAN_POINT('',(-91.94444444444,17.9,8.)); -#934 = VECTOR('',#935,1.); -#935 = DIRECTION('',(-0.,1.,0.)); -#936 = ORIENTED_EDGE('',*,*,#937,.T.); -#937 = EDGE_CURVE('',#930,#938,#940,.T.); -#938 = VERTEX_POINT('',#939); -#939 = CARTESIAN_POINT('',(-91.94444444444,20.1,40.)); -#940 = LINE('',#941,#942); -#941 = CARTESIAN_POINT('',(-91.94444444444,20.1,8.)); -#942 = VECTOR('',#943,1.); -#943 = DIRECTION('',(0.,0.,1.)); -#944 = ORIENTED_EDGE('',*,*,#945,.F.); -#945 = EDGE_CURVE('',#922,#938,#946,.T.); -#946 = LINE('',#947,#948); -#947 = CARTESIAN_POINT('',(-91.94444444444,17.9,40.)); -#948 = VECTOR('',#949,1.); -#949 = DIRECTION('',(-0.,1.,0.)); -#950 = PLANE('',#951); -#951 = AXIS2_PLACEMENT_3D('',#952,#953,#954); -#952 = CARTESIAN_POINT('',(-91.94444444444,17.9,8.)); -#953 = DIRECTION('',(1.,0.,-0.)); -#954 = DIRECTION('',(0.,0.,1.)); -#955 = ADVANCED_FACE('',(#956),#972,.F.); -#956 = FACE_BOUND('',#957,.F.); -#957 = EDGE_LOOP('',(#958,#964,#965,#971)); -#958 = ORIENTED_EDGE('',*,*,#959,.F.); -#959 = EDGE_CURVE('',#880,#920,#960,.T.); -#960 = LINE('',#961,#962); -#961 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#962 = VECTOR('',#963,1.); -#963 = DIRECTION('',(1.,0.,-0.)); -#964 = ORIENTED_EDGE('',*,*,#879,.T.); -#965 = ORIENTED_EDGE('',*,*,#966,.T.); -#966 = EDGE_CURVE('',#882,#922,#967,.T.); -#967 = LINE('',#968,#969); -#968 = CARTESIAN_POINT('',(-96.94444444444,17.9,40.)); -#969 = VECTOR('',#970,1.); -#970 = DIRECTION('',(1.,0.,-0.)); -#971 = ORIENTED_EDGE('',*,*,#919,.F.); -#972 = PLANE('',#973); -#973 = AXIS2_PLACEMENT_3D('',#974,#975,#976); -#974 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#975 = DIRECTION('',(-0.,1.,0.)); -#976 = DIRECTION('',(0.,0.,1.)); -#977 = ADVANCED_FACE('',(#978),#994,.T.); -#978 = FACE_BOUND('',#979,.T.); -#979 = EDGE_LOOP('',(#980,#986,#987,#993)); -#980 = ORIENTED_EDGE('',*,*,#981,.F.); -#981 = EDGE_CURVE('',#890,#930,#982,.T.); -#982 = LINE('',#983,#984); -#983 = CARTESIAN_POINT('',(-96.94444444444,20.1,8.)); -#984 = VECTOR('',#985,1.); -#985 = DIRECTION('',(1.,0.,-0.)); -#986 = ORIENTED_EDGE('',*,*,#897,.T.); -#987 = ORIENTED_EDGE('',*,*,#988,.T.); -#988 = EDGE_CURVE('',#898,#938,#989,.T.); +#816 = DIRECTION('',(-0.,1.,0.)); +#817 = PLANE('',#818); +#818 = AXIS2_PLACEMENT_3D('',#819,#820,#821); +#819 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#820 = DIRECTION('',(0.,0.,1.)); +#821 = DIRECTION('',(1.,0.,-0.)); +#822 = ADVANCED_FACE('',(#823),#1415,.T.); +#823 = FACE_BOUND('',#824,.T.); +#824 = EDGE_LOOP('',(#825,#833,#839,#840,#841,#849,#857,#865,#873,#881, + #889,#897,#905,#913,#921,#929,#937,#945,#953,#961,#969,#977,#985, + #993,#1001,#1009,#1017,#1025,#1033,#1041,#1049,#1057,#1065,#1073, + #1081,#1089,#1097,#1105,#1113,#1121,#1129,#1137,#1145,#1153,#1161, + #1169,#1177,#1185,#1193,#1201,#1209,#1217,#1225,#1233,#1241,#1249, + #1257,#1265,#1273,#1281,#1289,#1297,#1305,#1313,#1321,#1329,#1337, + #1345,#1353,#1361,#1369,#1377,#1385,#1393,#1401,#1409)); +#825 = ORIENTED_EDGE('',*,*,#826,.F.); +#826 = EDGE_CURVE('',#181,#827,#829,.T.); +#827 = VERTEX_POINT('',#828); +#828 = CARTESIAN_POINT('',(100.,-18.,40.)); +#829 = LINE('',#830,#831); +#830 = CARTESIAN_POINT('',(100.,-18.,0.)); +#831 = VECTOR('',#832,1.); +#832 = DIRECTION('',(0.,0.,1.)); +#833 = ORIENTED_EDGE('',*,*,#834,.T.); +#834 = EDGE_CURVE('',#181,#40,#835,.T.); +#835 = LINE('',#836,#837); +#836 = CARTESIAN_POINT('',(-100.,-18.,2.)); +#837 = VECTOR('',#838,1.); +#838 = DIRECTION('',(-1.,-0.,0.)); +#839 = ORIENTED_EDGE('',*,*,#101,.T.); +#840 = ORIENTED_EDGE('',*,*,#802,.T.); +#841 = ORIENTED_EDGE('',*,*,#842,.T.); +#842 = EDGE_CURVE('',#803,#843,#845,.T.); +#843 = VERTEX_POINT('',#844); +#844 = CARTESIAN_POINT('',(-96.94444444444,-18.,8.)); +#845 = LINE('',#846,#847); +#846 = CARTESIAN_POINT('',(-96.94444444444,-18.,4.)); +#847 = VECTOR('',#848,1.); +#848 = DIRECTION('',(-0.,0.,-1.)); +#849 = ORIENTED_EDGE('',*,*,#850,.T.); +#850 = EDGE_CURVE('',#843,#851,#853,.T.); +#851 = VERTEX_POINT('',#852); +#852 = CARTESIAN_POINT('',(-91.94444444444,-18.,8.)); +#853 = LINE('',#854,#855); +#854 = CARTESIAN_POINT('',(-98.47222222222,-18.,8.)); +#855 = VECTOR('',#856,1.); +#856 = DIRECTION('',(1.,0.,-0.)); +#857 = ORIENTED_EDGE('',*,*,#858,.F.); +#858 = EDGE_CURVE('',#859,#851,#861,.T.); +#859 = VERTEX_POINT('',#860); +#860 = CARTESIAN_POINT('',(-91.94444444444,-18.,40.)); +#861 = LINE('',#862,#863); +#862 = CARTESIAN_POINT('',(-91.94444444444,-18.,4.)); +#863 = VECTOR('',#864,1.); +#864 = DIRECTION('',(-0.,0.,-1.)); +#865 = ORIENTED_EDGE('',*,*,#866,.T.); +#866 = EDGE_CURVE('',#859,#867,#869,.T.); +#867 = VERTEX_POINT('',#868); +#868 = CARTESIAN_POINT('',(-85.83333333333,-18.,40.)); +#869 = LINE('',#870,#871); +#870 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#871 = VECTOR('',#872,1.); +#872 = DIRECTION('',(1.,0.,-0.)); +#873 = ORIENTED_EDGE('',*,*,#874,.T.); +#874 = EDGE_CURVE('',#867,#875,#877,.T.); +#875 = VERTEX_POINT('',#876); +#876 = CARTESIAN_POINT('',(-85.83333333333,-18.,8.)); +#877 = LINE('',#878,#879); +#878 = CARTESIAN_POINT('',(-85.83333333333,-18.,4.)); +#879 = VECTOR('',#880,1.); +#880 = DIRECTION('',(-0.,0.,-1.)); +#881 = ORIENTED_EDGE('',*,*,#882,.T.); +#882 = EDGE_CURVE('',#875,#883,#885,.T.); +#883 = VERTEX_POINT('',#884); +#884 = CARTESIAN_POINT('',(-80.83333333333,-18.,8.)); +#885 = LINE('',#886,#887); +#886 = CARTESIAN_POINT('',(-92.91666666666,-18.,8.)); +#887 = VECTOR('',#888,1.); +#888 = DIRECTION('',(1.,0.,-0.)); +#889 = ORIENTED_EDGE('',*,*,#890,.F.); +#890 = EDGE_CURVE('',#891,#883,#893,.T.); +#891 = VERTEX_POINT('',#892); +#892 = CARTESIAN_POINT('',(-80.83333333333,-18.,40.)); +#893 = LINE('',#894,#895); +#894 = CARTESIAN_POINT('',(-80.83333333333,-18.,4.)); +#895 = VECTOR('',#896,1.); +#896 = DIRECTION('',(-0.,0.,-1.)); +#897 = ORIENTED_EDGE('',*,*,#898,.T.); +#898 = EDGE_CURVE('',#891,#899,#901,.T.); +#899 = VERTEX_POINT('',#900); +#900 = CARTESIAN_POINT('',(-74.72222222222,-18.,40.)); +#901 = LINE('',#902,#903); +#902 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#903 = VECTOR('',#904,1.); +#904 = DIRECTION('',(1.,0.,-0.)); +#905 = ORIENTED_EDGE('',*,*,#906,.T.); +#906 = EDGE_CURVE('',#899,#907,#909,.T.); +#907 = VERTEX_POINT('',#908); +#908 = CARTESIAN_POINT('',(-74.72222222222,-18.,8.)); +#909 = LINE('',#910,#911); +#910 = CARTESIAN_POINT('',(-74.72222222222,-18.,4.)); +#911 = VECTOR('',#912,1.); +#912 = DIRECTION('',(-0.,0.,-1.)); +#913 = ORIENTED_EDGE('',*,*,#914,.T.); +#914 = EDGE_CURVE('',#907,#915,#917,.T.); +#915 = VERTEX_POINT('',#916); +#916 = CARTESIAN_POINT('',(-69.72222222222,-18.,8.)); +#917 = LINE('',#918,#919); +#918 = CARTESIAN_POINT('',(-87.36111111111,-18.,8.)); +#919 = VECTOR('',#920,1.); +#920 = DIRECTION('',(1.,0.,-0.)); +#921 = ORIENTED_EDGE('',*,*,#922,.F.); +#922 = EDGE_CURVE('',#923,#915,#925,.T.); +#923 = VERTEX_POINT('',#924); +#924 = CARTESIAN_POINT('',(-69.72222222222,-18.,40.)); +#925 = LINE('',#926,#927); +#926 = CARTESIAN_POINT('',(-69.72222222222,-18.,4.)); +#927 = VECTOR('',#928,1.); +#928 = DIRECTION('',(-0.,0.,-1.)); +#929 = ORIENTED_EDGE('',*,*,#930,.T.); +#930 = EDGE_CURVE('',#923,#931,#933,.T.); +#931 = VERTEX_POINT('',#932); +#932 = CARTESIAN_POINT('',(-63.61111111111,-18.,40.)); +#933 = LINE('',#934,#935); +#934 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#935 = VECTOR('',#936,1.); +#936 = DIRECTION('',(1.,0.,-0.)); +#937 = ORIENTED_EDGE('',*,*,#938,.T.); +#938 = EDGE_CURVE('',#931,#939,#941,.T.); +#939 = VERTEX_POINT('',#940); +#940 = CARTESIAN_POINT('',(-63.61111111111,-18.,8.)); +#941 = LINE('',#942,#943); +#942 = CARTESIAN_POINT('',(-63.61111111111,-18.,4.)); +#943 = VECTOR('',#944,1.); +#944 = DIRECTION('',(-0.,0.,-1.)); +#945 = ORIENTED_EDGE('',*,*,#946,.T.); +#946 = EDGE_CURVE('',#939,#947,#949,.T.); +#947 = VERTEX_POINT('',#948); +#948 = CARTESIAN_POINT('',(-58.61111111111,-18.,8.)); +#949 = LINE('',#950,#951); +#950 = CARTESIAN_POINT('',(-81.80555555555,-18.,8.)); +#951 = VECTOR('',#952,1.); +#952 = DIRECTION('',(1.,0.,-0.)); +#953 = ORIENTED_EDGE('',*,*,#954,.F.); +#954 = EDGE_CURVE('',#955,#947,#957,.T.); +#955 = VERTEX_POINT('',#956); +#956 = CARTESIAN_POINT('',(-58.61111111111,-18.,40.)); +#957 = LINE('',#958,#959); +#958 = CARTESIAN_POINT('',(-58.61111111111,-18.,4.)); +#959 = VECTOR('',#960,1.); +#960 = DIRECTION('',(-0.,0.,-1.)); +#961 = ORIENTED_EDGE('',*,*,#962,.T.); +#962 = EDGE_CURVE('',#955,#963,#965,.T.); +#963 = VERTEX_POINT('',#964); +#964 = CARTESIAN_POINT('',(-52.5,-18.,40.)); +#965 = LINE('',#966,#967); +#966 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#967 = VECTOR('',#968,1.); +#968 = DIRECTION('',(1.,0.,-0.)); +#969 = ORIENTED_EDGE('',*,*,#970,.T.); +#970 = EDGE_CURVE('',#963,#971,#973,.T.); +#971 = VERTEX_POINT('',#972); +#972 = CARTESIAN_POINT('',(-52.5,-18.,8.)); +#973 = LINE('',#974,#975); +#974 = CARTESIAN_POINT('',(-52.5,-18.,4.)); +#975 = VECTOR('',#976,1.); +#976 = DIRECTION('',(-0.,0.,-1.)); +#977 = ORIENTED_EDGE('',*,*,#978,.T.); +#978 = EDGE_CURVE('',#971,#979,#981,.T.); +#979 = VERTEX_POINT('',#980); +#980 = CARTESIAN_POINT('',(-47.5,-18.,8.)); +#981 = LINE('',#982,#983); +#982 = CARTESIAN_POINT('',(-76.25,-18.,8.)); +#983 = VECTOR('',#984,1.); +#984 = DIRECTION('',(1.,0.,-0.)); +#985 = ORIENTED_EDGE('',*,*,#986,.F.); +#986 = EDGE_CURVE('',#987,#979,#989,.T.); +#987 = VERTEX_POINT('',#988); +#988 = CARTESIAN_POINT('',(-47.5,-18.,40.)); #989 = LINE('',#990,#991); -#990 = CARTESIAN_POINT('',(-96.94444444444,20.1,40.)); +#990 = CARTESIAN_POINT('',(-47.5,-18.,4.)); #991 = VECTOR('',#992,1.); -#992 = DIRECTION('',(1.,0.,-0.)); -#993 = ORIENTED_EDGE('',*,*,#937,.F.); -#994 = PLANE('',#995); -#995 = AXIS2_PLACEMENT_3D('',#996,#997,#998); -#996 = CARTESIAN_POINT('',(-96.94444444444,20.1,8.)); -#997 = DIRECTION('',(-0.,1.,0.)); -#998 = DIRECTION('',(0.,0.,1.)); -#999 = ADVANCED_FACE('',(#1000),#1006,.F.); -#1000 = FACE_BOUND('',#1001,.F.); -#1001 = EDGE_LOOP('',(#1002,#1003,#1004,#1005)); -#1002 = ORIENTED_EDGE('',*,*,#889,.F.); -#1003 = ORIENTED_EDGE('',*,*,#959,.T.); -#1004 = ORIENTED_EDGE('',*,*,#929,.T.); -#1005 = ORIENTED_EDGE('',*,*,#981,.F.); -#1006 = PLANE('',#1007); -#1007 = AXIS2_PLACEMENT_3D('',#1008,#1009,#1010); -#1008 = CARTESIAN_POINT('',(-96.94444444444,17.9,8.)); -#1009 = DIRECTION('',(0.,0.,1.)); -#1010 = DIRECTION('',(1.,0.,-0.)); -#1011 = ADVANCED_FACE('',(#1012),#1018,.T.); -#1012 = FACE_BOUND('',#1013,.T.); -#1013 = EDGE_LOOP('',(#1014,#1015,#1016,#1017)); -#1014 = ORIENTED_EDGE('',*,*,#905,.F.); -#1015 = ORIENTED_EDGE('',*,*,#966,.T.); -#1016 = ORIENTED_EDGE('',*,*,#945,.T.); -#1017 = ORIENTED_EDGE('',*,*,#988,.F.); -#1018 = PLANE('',#1019); -#1019 = AXIS2_PLACEMENT_3D('',#1020,#1021,#1022); -#1020 = CARTESIAN_POINT('',(-96.94444444444,17.9,40.)); -#1021 = DIRECTION('',(0.,0.,1.)); -#1022 = DIRECTION('',(1.,0.,-0.)); -#1023 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1027)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1024,#1025,#1026)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1024 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1025 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1026 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1027 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1024, - 'distance_accuracy_value','confusion accuracy'); -#1028 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1029,#1031); -#1029 = ( REPRESENTATION_RELATIONSHIP('','',#872,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1030) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#1030 = ITEM_DEFINED_TRANSFORMATION('','',#11,#31); -#1031 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #1032); -#1032 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('5','WireDuct_RightCombSlot_01', - '',#5,#867,$); -#1033 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#869)); -#1034 = SHAPE_DEFINITION_REPRESENTATION(#1035,#1041); -#1035 = PRODUCT_DEFINITION_SHAPE('','',#1036); -#1036 = PRODUCT_DEFINITION('design','',#1037,#1040); -#1037 = PRODUCT_DEFINITION_FORMATION('','',#1038); -#1038 = PRODUCT('WireDuct_LeftCombSlot_02','WireDuct_LeftCombSlot_02','' - ,(#1039)); -#1039 = PRODUCT_CONTEXT('',#2,'mechanical'); -#1040 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#1041 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1042),#1192); -#1042 = MANIFOLD_SOLID_BREP('',#1043); -#1043 = CLOSED_SHELL('',(#1044,#1084,#1124,#1146,#1168,#1180)); -#1044 = ADVANCED_FACE('',(#1045),#1079,.F.); -#1045 = FACE_BOUND('',#1046,.F.); -#1046 = EDGE_LOOP('',(#1047,#1057,#1065,#1073)); -#1047 = ORIENTED_EDGE('',*,*,#1048,.F.); -#1048 = EDGE_CURVE('',#1049,#1051,#1053,.T.); -#1049 = VERTEX_POINT('',#1050); -#1050 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); +#992 = DIRECTION('',(-0.,0.,-1.)); +#993 = ORIENTED_EDGE('',*,*,#994,.T.); +#994 = EDGE_CURVE('',#987,#995,#997,.T.); +#995 = VERTEX_POINT('',#996); +#996 = CARTESIAN_POINT('',(-41.38888888888,-18.,40.)); +#997 = LINE('',#998,#999); +#998 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#999 = VECTOR('',#1000,1.); +#1000 = DIRECTION('',(1.,0.,-0.)); +#1001 = ORIENTED_EDGE('',*,*,#1002,.T.); +#1002 = EDGE_CURVE('',#995,#1003,#1005,.T.); +#1003 = VERTEX_POINT('',#1004); +#1004 = CARTESIAN_POINT('',(-41.38888888888,-18.,8.)); +#1005 = LINE('',#1006,#1007); +#1006 = CARTESIAN_POINT('',(-41.38888888888,-18.,4.)); +#1007 = VECTOR('',#1008,1.); +#1008 = DIRECTION('',(-0.,0.,-1.)); +#1009 = ORIENTED_EDGE('',*,*,#1010,.T.); +#1010 = EDGE_CURVE('',#1003,#1011,#1013,.T.); +#1011 = VERTEX_POINT('',#1012); +#1012 = CARTESIAN_POINT('',(-36.38888888888,-18.,8.)); +#1013 = LINE('',#1014,#1015); +#1014 = CARTESIAN_POINT('',(-70.69444444444,-18.,8.)); +#1015 = VECTOR('',#1016,1.); +#1016 = DIRECTION('',(1.,0.,-0.)); +#1017 = ORIENTED_EDGE('',*,*,#1018,.F.); +#1018 = EDGE_CURVE('',#1019,#1011,#1021,.T.); +#1019 = VERTEX_POINT('',#1020); +#1020 = CARTESIAN_POINT('',(-36.38888888888,-18.,40.)); +#1021 = LINE('',#1022,#1023); +#1022 = CARTESIAN_POINT('',(-36.38888888888,-18.,4.)); +#1023 = VECTOR('',#1024,1.); +#1024 = DIRECTION('',(-0.,0.,-1.)); +#1025 = ORIENTED_EDGE('',*,*,#1026,.T.); +#1026 = EDGE_CURVE('',#1019,#1027,#1029,.T.); +#1027 = VERTEX_POINT('',#1028); +#1028 = CARTESIAN_POINT('',(-30.27777777777,-18.,40.)); +#1029 = LINE('',#1030,#1031); +#1030 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1031 = VECTOR('',#1032,1.); +#1032 = DIRECTION('',(1.,0.,-0.)); +#1033 = ORIENTED_EDGE('',*,*,#1034,.T.); +#1034 = EDGE_CURVE('',#1027,#1035,#1037,.T.); +#1035 = VERTEX_POINT('',#1036); +#1036 = CARTESIAN_POINT('',(-30.27777777777,-18.,8.)); +#1037 = LINE('',#1038,#1039); +#1038 = CARTESIAN_POINT('',(-30.27777777777,-18.,4.)); +#1039 = VECTOR('',#1040,1.); +#1040 = DIRECTION('',(-0.,0.,-1.)); +#1041 = ORIENTED_EDGE('',*,*,#1042,.T.); +#1042 = EDGE_CURVE('',#1035,#1043,#1045,.T.); +#1043 = VERTEX_POINT('',#1044); +#1044 = CARTESIAN_POINT('',(-25.27777777777,-18.,8.)); +#1045 = LINE('',#1046,#1047); +#1046 = CARTESIAN_POINT('',(-65.13888888888,-18.,8.)); +#1047 = VECTOR('',#1048,1.); +#1048 = DIRECTION('',(1.,0.,-0.)); +#1049 = ORIENTED_EDGE('',*,*,#1050,.F.); +#1050 = EDGE_CURVE('',#1051,#1043,#1053,.T.); #1051 = VERTEX_POINT('',#1052); -#1052 = CARTESIAN_POINT('',(-85.83333333333,-20.1,40.)); +#1052 = CARTESIAN_POINT('',(-25.27777777777,-18.,40.)); #1053 = LINE('',#1054,#1055); -#1054 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); +#1054 = CARTESIAN_POINT('',(-25.27777777777,-18.,4.)); #1055 = VECTOR('',#1056,1.); -#1056 = DIRECTION('',(0.,0.,1.)); +#1056 = DIRECTION('',(-0.,0.,-1.)); #1057 = ORIENTED_EDGE('',*,*,#1058,.T.); -#1058 = EDGE_CURVE('',#1049,#1059,#1061,.T.); +#1058 = EDGE_CURVE('',#1051,#1059,#1061,.T.); #1059 = VERTEX_POINT('',#1060); -#1060 = CARTESIAN_POINT('',(-85.83333333333,-17.9,8.)); +#1060 = CARTESIAN_POINT('',(-19.16666666666,-18.,40.)); #1061 = LINE('',#1062,#1063); -#1062 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); +#1062 = CARTESIAN_POINT('',(-100.,-18.,40.)); #1063 = VECTOR('',#1064,1.); -#1064 = DIRECTION('',(-0.,1.,0.)); +#1064 = DIRECTION('',(1.,0.,-0.)); #1065 = ORIENTED_EDGE('',*,*,#1066,.T.); #1066 = EDGE_CURVE('',#1059,#1067,#1069,.T.); #1067 = VERTEX_POINT('',#1068); -#1068 = CARTESIAN_POINT('',(-85.83333333333,-17.9,40.)); +#1068 = CARTESIAN_POINT('',(-19.16666666666,-18.,8.)); #1069 = LINE('',#1070,#1071); -#1070 = CARTESIAN_POINT('',(-85.83333333333,-17.9,8.)); +#1070 = CARTESIAN_POINT('',(-19.16666666666,-18.,4.)); #1071 = VECTOR('',#1072,1.); -#1072 = DIRECTION('',(0.,0.,1.)); -#1073 = ORIENTED_EDGE('',*,*,#1074,.F.); -#1074 = EDGE_CURVE('',#1051,#1067,#1075,.T.); -#1075 = LINE('',#1076,#1077); -#1076 = CARTESIAN_POINT('',(-85.83333333333,-20.1,40.)); -#1077 = VECTOR('',#1078,1.); -#1078 = DIRECTION('',(-0.,1.,0.)); -#1079 = PLANE('',#1080); -#1080 = AXIS2_PLACEMENT_3D('',#1081,#1082,#1083); -#1081 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); -#1082 = DIRECTION('',(1.,0.,-0.)); -#1083 = DIRECTION('',(0.,0.,1.)); -#1084 = ADVANCED_FACE('',(#1085),#1119,.T.); -#1085 = FACE_BOUND('',#1086,.T.); -#1086 = EDGE_LOOP('',(#1087,#1097,#1105,#1113)); -#1087 = ORIENTED_EDGE('',*,*,#1088,.F.); -#1088 = EDGE_CURVE('',#1089,#1091,#1093,.T.); -#1089 = VERTEX_POINT('',#1090); -#1090 = CARTESIAN_POINT('',(-80.83333333333,-20.1,8.)); +#1072 = DIRECTION('',(-0.,0.,-1.)); +#1073 = ORIENTED_EDGE('',*,*,#1074,.T.); +#1074 = EDGE_CURVE('',#1067,#1075,#1077,.T.); +#1075 = VERTEX_POINT('',#1076); +#1076 = CARTESIAN_POINT('',(-14.16666666666,-18.,8.)); +#1077 = LINE('',#1078,#1079); +#1078 = CARTESIAN_POINT('',(-59.58333333333,-18.,8.)); +#1079 = VECTOR('',#1080,1.); +#1080 = DIRECTION('',(1.,0.,-0.)); +#1081 = ORIENTED_EDGE('',*,*,#1082,.F.); +#1082 = EDGE_CURVE('',#1083,#1075,#1085,.T.); +#1083 = VERTEX_POINT('',#1084); +#1084 = CARTESIAN_POINT('',(-14.16666666666,-18.,40.)); +#1085 = LINE('',#1086,#1087); +#1086 = CARTESIAN_POINT('',(-14.16666666666,-18.,4.)); +#1087 = VECTOR('',#1088,1.); +#1088 = DIRECTION('',(-0.,0.,-1.)); +#1089 = ORIENTED_EDGE('',*,*,#1090,.T.); +#1090 = EDGE_CURVE('',#1083,#1091,#1093,.T.); #1091 = VERTEX_POINT('',#1092); -#1092 = CARTESIAN_POINT('',(-80.83333333333,-20.1,40.)); +#1092 = CARTESIAN_POINT('',(-8.055555555556,-18.,40.)); #1093 = LINE('',#1094,#1095); -#1094 = CARTESIAN_POINT('',(-80.83333333333,-20.1,8.)); +#1094 = CARTESIAN_POINT('',(-100.,-18.,40.)); #1095 = VECTOR('',#1096,1.); -#1096 = DIRECTION('',(0.,0.,1.)); +#1096 = DIRECTION('',(1.,0.,-0.)); #1097 = ORIENTED_EDGE('',*,*,#1098,.T.); -#1098 = EDGE_CURVE('',#1089,#1099,#1101,.T.); +#1098 = EDGE_CURVE('',#1091,#1099,#1101,.T.); #1099 = VERTEX_POINT('',#1100); -#1100 = CARTESIAN_POINT('',(-80.83333333333,-17.9,8.)); +#1100 = CARTESIAN_POINT('',(-8.055555555556,-18.,8.)); #1101 = LINE('',#1102,#1103); -#1102 = CARTESIAN_POINT('',(-80.83333333333,-20.1,8.)); +#1102 = CARTESIAN_POINT('',(-8.055555555556,-18.,4.)); #1103 = VECTOR('',#1104,1.); -#1104 = DIRECTION('',(-0.,1.,0.)); +#1104 = DIRECTION('',(-0.,0.,-1.)); #1105 = ORIENTED_EDGE('',*,*,#1106,.T.); #1106 = EDGE_CURVE('',#1099,#1107,#1109,.T.); #1107 = VERTEX_POINT('',#1108); -#1108 = CARTESIAN_POINT('',(-80.83333333333,-17.9,40.)); +#1108 = CARTESIAN_POINT('',(-3.055555555556,-18.,8.)); #1109 = LINE('',#1110,#1111); -#1110 = CARTESIAN_POINT('',(-80.83333333333,-17.9,8.)); +#1110 = CARTESIAN_POINT('',(-54.02777777777,-18.,8.)); #1111 = VECTOR('',#1112,1.); -#1112 = DIRECTION('',(0.,0.,1.)); +#1112 = DIRECTION('',(1.,0.,-0.)); #1113 = ORIENTED_EDGE('',*,*,#1114,.F.); -#1114 = EDGE_CURVE('',#1091,#1107,#1115,.T.); -#1115 = LINE('',#1116,#1117); -#1116 = CARTESIAN_POINT('',(-80.83333333333,-20.1,40.)); -#1117 = VECTOR('',#1118,1.); -#1118 = DIRECTION('',(-0.,1.,0.)); -#1119 = PLANE('',#1120); -#1120 = AXIS2_PLACEMENT_3D('',#1121,#1122,#1123); -#1121 = CARTESIAN_POINT('',(-80.83333333333,-20.1,8.)); -#1122 = DIRECTION('',(1.,0.,-0.)); -#1123 = DIRECTION('',(0.,0.,1.)); -#1124 = ADVANCED_FACE('',(#1125),#1141,.F.); -#1125 = FACE_BOUND('',#1126,.F.); -#1126 = EDGE_LOOP('',(#1127,#1133,#1134,#1140)); -#1127 = ORIENTED_EDGE('',*,*,#1128,.F.); -#1128 = EDGE_CURVE('',#1049,#1089,#1129,.T.); -#1129 = LINE('',#1130,#1131); -#1130 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); -#1131 = VECTOR('',#1132,1.); -#1132 = DIRECTION('',(1.,0.,-0.)); -#1133 = ORIENTED_EDGE('',*,*,#1048,.T.); -#1134 = ORIENTED_EDGE('',*,*,#1135,.T.); -#1135 = EDGE_CURVE('',#1051,#1091,#1136,.T.); -#1136 = LINE('',#1137,#1138); -#1137 = CARTESIAN_POINT('',(-85.83333333333,-20.1,40.)); -#1138 = VECTOR('',#1139,1.); -#1139 = DIRECTION('',(1.,0.,-0.)); -#1140 = ORIENTED_EDGE('',*,*,#1088,.F.); -#1141 = PLANE('',#1142); -#1142 = AXIS2_PLACEMENT_3D('',#1143,#1144,#1145); -#1143 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); -#1144 = DIRECTION('',(-0.,1.,0.)); -#1145 = DIRECTION('',(0.,0.,1.)); -#1146 = ADVANCED_FACE('',(#1147),#1163,.T.); -#1147 = FACE_BOUND('',#1148,.T.); -#1148 = EDGE_LOOP('',(#1149,#1155,#1156,#1162)); -#1149 = ORIENTED_EDGE('',*,*,#1150,.F.); -#1150 = EDGE_CURVE('',#1059,#1099,#1151,.T.); -#1151 = LINE('',#1152,#1153); -#1152 = CARTESIAN_POINT('',(-85.83333333333,-17.9,8.)); -#1153 = VECTOR('',#1154,1.); -#1154 = DIRECTION('',(1.,0.,-0.)); -#1155 = ORIENTED_EDGE('',*,*,#1066,.T.); -#1156 = ORIENTED_EDGE('',*,*,#1157,.T.); -#1157 = EDGE_CURVE('',#1067,#1107,#1158,.T.); -#1158 = LINE('',#1159,#1160); -#1159 = CARTESIAN_POINT('',(-85.83333333333,-17.9,40.)); -#1160 = VECTOR('',#1161,1.); -#1161 = DIRECTION('',(1.,0.,-0.)); -#1162 = ORIENTED_EDGE('',*,*,#1106,.F.); -#1163 = PLANE('',#1164); -#1164 = AXIS2_PLACEMENT_3D('',#1165,#1166,#1167); -#1165 = CARTESIAN_POINT('',(-85.83333333333,-17.9,8.)); -#1166 = DIRECTION('',(-0.,1.,0.)); -#1167 = DIRECTION('',(0.,0.,1.)); -#1168 = ADVANCED_FACE('',(#1169),#1175,.F.); -#1169 = FACE_BOUND('',#1170,.F.); -#1170 = EDGE_LOOP('',(#1171,#1172,#1173,#1174)); -#1171 = ORIENTED_EDGE('',*,*,#1058,.F.); -#1172 = ORIENTED_EDGE('',*,*,#1128,.T.); -#1173 = ORIENTED_EDGE('',*,*,#1098,.T.); -#1174 = ORIENTED_EDGE('',*,*,#1150,.F.); -#1175 = PLANE('',#1176); -#1176 = AXIS2_PLACEMENT_3D('',#1177,#1178,#1179); -#1177 = CARTESIAN_POINT('',(-85.83333333333,-20.1,8.)); -#1178 = DIRECTION('',(0.,0.,1.)); -#1179 = DIRECTION('',(1.,0.,-0.)); -#1180 = ADVANCED_FACE('',(#1181),#1187,.T.); -#1181 = FACE_BOUND('',#1182,.T.); -#1182 = EDGE_LOOP('',(#1183,#1184,#1185,#1186)); -#1183 = ORIENTED_EDGE('',*,*,#1074,.F.); -#1184 = ORIENTED_EDGE('',*,*,#1135,.T.); -#1185 = ORIENTED_EDGE('',*,*,#1114,.T.); -#1186 = ORIENTED_EDGE('',*,*,#1157,.F.); -#1187 = PLANE('',#1188); -#1188 = AXIS2_PLACEMENT_3D('',#1189,#1190,#1191); -#1189 = CARTESIAN_POINT('',(-85.83333333333,-20.1,40.)); -#1190 = DIRECTION('',(0.,0.,1.)); -#1191 = DIRECTION('',(1.,0.,-0.)); -#1192 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1196)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1193,#1194,#1195)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1193 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1194 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1195 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1196 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1193, - 'distance_accuracy_value','confusion accuracy'); -#1197 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1198,#1200); -#1198 = ( REPRESENTATION_RELATIONSHIP('','',#1041,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1199) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#1199 = ITEM_DEFINED_TRANSFORMATION('','',#11,#35); -#1200 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #1201); -#1201 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('6','WireDuct_LeftCombSlot_02','' - ,#5,#1036,$); -#1202 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1038)); -#1203 = SHAPE_DEFINITION_REPRESENTATION(#1204,#1210); -#1204 = PRODUCT_DEFINITION_SHAPE('','',#1205); -#1205 = PRODUCT_DEFINITION('design','',#1206,#1209); -#1206 = PRODUCT_DEFINITION_FORMATION('','',#1207); -#1207 = PRODUCT('WireDuct_RightCombSlot_02','WireDuct_RightCombSlot_02', - '',(#1208)); -#1208 = PRODUCT_CONTEXT('',#2,'mechanical'); -#1209 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#1210 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1211),#1361); -#1211 = MANIFOLD_SOLID_BREP('',#1212); -#1212 = CLOSED_SHELL('',(#1213,#1253,#1293,#1315,#1337,#1349)); -#1213 = ADVANCED_FACE('',(#1214),#1248,.F.); -#1214 = FACE_BOUND('',#1215,.F.); -#1215 = EDGE_LOOP('',(#1216,#1226,#1234,#1242)); -#1216 = ORIENTED_EDGE('',*,*,#1217,.F.); -#1217 = EDGE_CURVE('',#1218,#1220,#1222,.T.); -#1218 = VERTEX_POINT('',#1219); -#1219 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1220 = VERTEX_POINT('',#1221); -#1221 = CARTESIAN_POINT('',(-85.83333333333,17.9,40.)); -#1222 = LINE('',#1223,#1224); -#1223 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1224 = VECTOR('',#1225,1.); -#1225 = DIRECTION('',(0.,0.,1.)); -#1226 = ORIENTED_EDGE('',*,*,#1227,.T.); -#1227 = EDGE_CURVE('',#1218,#1228,#1230,.T.); -#1228 = VERTEX_POINT('',#1229); -#1229 = CARTESIAN_POINT('',(-85.83333333333,20.1,8.)); -#1230 = LINE('',#1231,#1232); -#1231 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1232 = VECTOR('',#1233,1.); -#1233 = DIRECTION('',(-0.,1.,0.)); -#1234 = ORIENTED_EDGE('',*,*,#1235,.T.); -#1235 = EDGE_CURVE('',#1228,#1236,#1238,.T.); -#1236 = VERTEX_POINT('',#1237); -#1237 = CARTESIAN_POINT('',(-85.83333333333,20.1,40.)); -#1238 = LINE('',#1239,#1240); -#1239 = CARTESIAN_POINT('',(-85.83333333333,20.1,8.)); -#1240 = VECTOR('',#1241,1.); -#1241 = DIRECTION('',(0.,0.,1.)); -#1242 = ORIENTED_EDGE('',*,*,#1243,.F.); -#1243 = EDGE_CURVE('',#1220,#1236,#1244,.T.); -#1244 = LINE('',#1245,#1246); -#1245 = CARTESIAN_POINT('',(-85.83333333333,17.9,40.)); -#1246 = VECTOR('',#1247,1.); -#1247 = DIRECTION('',(-0.,1.,0.)); -#1248 = PLANE('',#1249); -#1249 = AXIS2_PLACEMENT_3D('',#1250,#1251,#1252); -#1250 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1251 = DIRECTION('',(1.,0.,-0.)); -#1252 = DIRECTION('',(0.,0.,1.)); -#1253 = ADVANCED_FACE('',(#1254),#1288,.T.); -#1254 = FACE_BOUND('',#1255,.T.); -#1255 = EDGE_LOOP('',(#1256,#1266,#1274,#1282)); -#1256 = ORIENTED_EDGE('',*,*,#1257,.F.); -#1257 = EDGE_CURVE('',#1258,#1260,#1262,.T.); -#1258 = VERTEX_POINT('',#1259); -#1259 = CARTESIAN_POINT('',(-80.83333333333,17.9,8.)); -#1260 = VERTEX_POINT('',#1261); -#1261 = CARTESIAN_POINT('',(-80.83333333333,17.9,40.)); -#1262 = LINE('',#1263,#1264); -#1263 = CARTESIAN_POINT('',(-80.83333333333,17.9,8.)); -#1264 = VECTOR('',#1265,1.); -#1265 = DIRECTION('',(0.,0.,1.)); -#1266 = ORIENTED_EDGE('',*,*,#1267,.T.); -#1267 = EDGE_CURVE('',#1258,#1268,#1270,.T.); -#1268 = VERTEX_POINT('',#1269); -#1269 = CARTESIAN_POINT('',(-80.83333333333,20.1,8.)); -#1270 = LINE('',#1271,#1272); -#1271 = CARTESIAN_POINT('',(-80.83333333333,17.9,8.)); -#1272 = VECTOR('',#1273,1.); -#1273 = DIRECTION('',(-0.,1.,0.)); -#1274 = ORIENTED_EDGE('',*,*,#1275,.T.); -#1275 = EDGE_CURVE('',#1268,#1276,#1278,.T.); -#1276 = VERTEX_POINT('',#1277); -#1277 = CARTESIAN_POINT('',(-80.83333333333,20.1,40.)); -#1278 = LINE('',#1279,#1280); -#1279 = CARTESIAN_POINT('',(-80.83333333333,20.1,8.)); -#1280 = VECTOR('',#1281,1.); -#1281 = DIRECTION('',(0.,0.,1.)); -#1282 = ORIENTED_EDGE('',*,*,#1283,.F.); -#1283 = EDGE_CURVE('',#1260,#1276,#1284,.T.); -#1284 = LINE('',#1285,#1286); -#1285 = CARTESIAN_POINT('',(-80.83333333333,17.9,40.)); -#1286 = VECTOR('',#1287,1.); -#1287 = DIRECTION('',(-0.,1.,0.)); -#1288 = PLANE('',#1289); -#1289 = AXIS2_PLACEMENT_3D('',#1290,#1291,#1292); -#1290 = CARTESIAN_POINT('',(-80.83333333333,17.9,8.)); -#1291 = DIRECTION('',(1.,0.,-0.)); -#1292 = DIRECTION('',(0.,0.,1.)); -#1293 = ADVANCED_FACE('',(#1294),#1310,.F.); -#1294 = FACE_BOUND('',#1295,.F.); -#1295 = EDGE_LOOP('',(#1296,#1302,#1303,#1309)); -#1296 = ORIENTED_EDGE('',*,*,#1297,.F.); -#1297 = EDGE_CURVE('',#1218,#1258,#1298,.T.); -#1298 = LINE('',#1299,#1300); -#1299 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1300 = VECTOR('',#1301,1.); -#1301 = DIRECTION('',(1.,0.,-0.)); -#1302 = ORIENTED_EDGE('',*,*,#1217,.T.); -#1303 = ORIENTED_EDGE('',*,*,#1304,.T.); -#1304 = EDGE_CURVE('',#1220,#1260,#1305,.T.); -#1305 = LINE('',#1306,#1307); -#1306 = CARTESIAN_POINT('',(-85.83333333333,17.9,40.)); -#1307 = VECTOR('',#1308,1.); -#1308 = DIRECTION('',(1.,0.,-0.)); -#1309 = ORIENTED_EDGE('',*,*,#1257,.F.); -#1310 = PLANE('',#1311); -#1311 = AXIS2_PLACEMENT_3D('',#1312,#1313,#1314); -#1312 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1313 = DIRECTION('',(-0.,1.,0.)); -#1314 = DIRECTION('',(0.,0.,1.)); -#1315 = ADVANCED_FACE('',(#1316),#1332,.T.); -#1316 = FACE_BOUND('',#1317,.T.); -#1317 = EDGE_LOOP('',(#1318,#1324,#1325,#1331)); -#1318 = ORIENTED_EDGE('',*,*,#1319,.F.); -#1319 = EDGE_CURVE('',#1228,#1268,#1320,.T.); -#1320 = LINE('',#1321,#1322); -#1321 = CARTESIAN_POINT('',(-85.83333333333,20.1,8.)); -#1322 = VECTOR('',#1323,1.); -#1323 = DIRECTION('',(1.,0.,-0.)); -#1324 = ORIENTED_EDGE('',*,*,#1235,.T.); -#1325 = ORIENTED_EDGE('',*,*,#1326,.T.); -#1326 = EDGE_CURVE('',#1236,#1276,#1327,.T.); -#1327 = LINE('',#1328,#1329); -#1328 = CARTESIAN_POINT('',(-85.83333333333,20.1,40.)); -#1329 = VECTOR('',#1330,1.); -#1330 = DIRECTION('',(1.,0.,-0.)); -#1331 = ORIENTED_EDGE('',*,*,#1275,.F.); -#1332 = PLANE('',#1333); -#1333 = AXIS2_PLACEMENT_3D('',#1334,#1335,#1336); -#1334 = CARTESIAN_POINT('',(-85.83333333333,20.1,8.)); -#1335 = DIRECTION('',(-0.,1.,0.)); -#1336 = DIRECTION('',(0.,0.,1.)); -#1337 = ADVANCED_FACE('',(#1338),#1344,.F.); -#1338 = FACE_BOUND('',#1339,.F.); -#1339 = EDGE_LOOP('',(#1340,#1341,#1342,#1343)); -#1340 = ORIENTED_EDGE('',*,*,#1227,.F.); -#1341 = ORIENTED_EDGE('',*,*,#1297,.T.); -#1342 = ORIENTED_EDGE('',*,*,#1267,.T.); -#1343 = ORIENTED_EDGE('',*,*,#1319,.F.); -#1344 = PLANE('',#1345); -#1345 = AXIS2_PLACEMENT_3D('',#1346,#1347,#1348); -#1346 = CARTESIAN_POINT('',(-85.83333333333,17.9,8.)); -#1347 = DIRECTION('',(0.,0.,1.)); -#1348 = DIRECTION('',(1.,0.,-0.)); -#1349 = ADVANCED_FACE('',(#1350),#1356,.T.); -#1350 = FACE_BOUND('',#1351,.T.); -#1351 = EDGE_LOOP('',(#1352,#1353,#1354,#1355)); -#1352 = ORIENTED_EDGE('',*,*,#1243,.F.); -#1353 = ORIENTED_EDGE('',*,*,#1304,.T.); -#1354 = ORIENTED_EDGE('',*,*,#1283,.T.); -#1355 = ORIENTED_EDGE('',*,*,#1326,.F.); -#1356 = PLANE('',#1357); -#1357 = AXIS2_PLACEMENT_3D('',#1358,#1359,#1360); -#1358 = CARTESIAN_POINT('',(-85.83333333333,17.9,40.)); -#1359 = DIRECTION('',(0.,0.,1.)); -#1360 = DIRECTION('',(1.,0.,-0.)); -#1361 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1365)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1362,#1363,#1364)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1362 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1363 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1364 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1365 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1362, - 'distance_accuracy_value','confusion accuracy'); -#1366 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1367,#1369); -#1367 = ( REPRESENTATION_RELATIONSHIP('','',#1210,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1368) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#1368 = ITEM_DEFINED_TRANSFORMATION('','',#11,#39); -#1369 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #1370); -#1370 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('7','WireDuct_RightCombSlot_02', - '',#5,#1205,$); -#1371 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1207)); -#1372 = SHAPE_DEFINITION_REPRESENTATION(#1373,#1379); -#1373 = PRODUCT_DEFINITION_SHAPE('','',#1374); -#1374 = PRODUCT_DEFINITION('design','',#1375,#1378); -#1375 = PRODUCT_DEFINITION_FORMATION('','',#1376); -#1376 = PRODUCT('WireDuct_LeftCombSlot_03','WireDuct_LeftCombSlot_03','' - ,(#1377)); -#1377 = PRODUCT_CONTEXT('',#2,'mechanical'); -#1378 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#1379 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1380),#1530); -#1380 = MANIFOLD_SOLID_BREP('',#1381); -#1381 = CLOSED_SHELL('',(#1382,#1422,#1462,#1484,#1506,#1518)); -#1382 = ADVANCED_FACE('',(#1383),#1417,.F.); -#1383 = FACE_BOUND('',#1384,.F.); -#1384 = EDGE_LOOP('',(#1385,#1395,#1403,#1411)); -#1385 = ORIENTED_EDGE('',*,*,#1386,.F.); -#1386 = EDGE_CURVE('',#1387,#1389,#1391,.T.); +#1114 = EDGE_CURVE('',#1115,#1107,#1117,.T.); +#1115 = VERTEX_POINT('',#1116); +#1116 = CARTESIAN_POINT('',(-3.055555555556,-18.,40.)); +#1117 = LINE('',#1118,#1119); +#1118 = CARTESIAN_POINT('',(-3.055555555556,-18.,4.)); +#1119 = VECTOR('',#1120,1.); +#1120 = DIRECTION('',(-0.,0.,-1.)); +#1121 = ORIENTED_EDGE('',*,*,#1122,.T.); +#1122 = EDGE_CURVE('',#1115,#1123,#1125,.T.); +#1123 = VERTEX_POINT('',#1124); +#1124 = CARTESIAN_POINT('',(3.055555555556,-18.,40.)); +#1125 = LINE('',#1126,#1127); +#1126 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1127 = VECTOR('',#1128,1.); +#1128 = DIRECTION('',(1.,0.,-0.)); +#1129 = ORIENTED_EDGE('',*,*,#1130,.T.); +#1130 = EDGE_CURVE('',#1123,#1131,#1133,.T.); +#1131 = VERTEX_POINT('',#1132); +#1132 = CARTESIAN_POINT('',(3.055555555556,-18.,8.)); +#1133 = LINE('',#1134,#1135); +#1134 = CARTESIAN_POINT('',(3.055555555556,-18.,4.)); +#1135 = VECTOR('',#1136,1.); +#1136 = DIRECTION('',(-0.,0.,-1.)); +#1137 = ORIENTED_EDGE('',*,*,#1138,.T.); +#1138 = EDGE_CURVE('',#1131,#1139,#1141,.T.); +#1139 = VERTEX_POINT('',#1140); +#1140 = CARTESIAN_POINT('',(8.055555555556,-18.,8.)); +#1141 = LINE('',#1142,#1143); +#1142 = CARTESIAN_POINT('',(-48.47222222222,-18.,8.)); +#1143 = VECTOR('',#1144,1.); +#1144 = DIRECTION('',(1.,0.,-0.)); +#1145 = ORIENTED_EDGE('',*,*,#1146,.F.); +#1146 = EDGE_CURVE('',#1147,#1139,#1149,.T.); +#1147 = VERTEX_POINT('',#1148); +#1148 = CARTESIAN_POINT('',(8.055555555556,-18.,40.)); +#1149 = LINE('',#1150,#1151); +#1150 = CARTESIAN_POINT('',(8.055555555556,-18.,4.)); +#1151 = VECTOR('',#1152,1.); +#1152 = DIRECTION('',(-0.,0.,-1.)); +#1153 = ORIENTED_EDGE('',*,*,#1154,.T.); +#1154 = EDGE_CURVE('',#1147,#1155,#1157,.T.); +#1155 = VERTEX_POINT('',#1156); +#1156 = CARTESIAN_POINT('',(14.166666666667,-18.,40.)); +#1157 = LINE('',#1158,#1159); +#1158 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1159 = VECTOR('',#1160,1.); +#1160 = DIRECTION('',(1.,0.,-0.)); +#1161 = ORIENTED_EDGE('',*,*,#1162,.T.); +#1162 = EDGE_CURVE('',#1155,#1163,#1165,.T.); +#1163 = VERTEX_POINT('',#1164); +#1164 = CARTESIAN_POINT('',(14.166666666667,-18.,8.)); +#1165 = LINE('',#1166,#1167); +#1166 = CARTESIAN_POINT('',(14.166666666667,-18.,4.)); +#1167 = VECTOR('',#1168,1.); +#1168 = DIRECTION('',(-0.,0.,-1.)); +#1169 = ORIENTED_EDGE('',*,*,#1170,.T.); +#1170 = EDGE_CURVE('',#1163,#1171,#1173,.T.); +#1171 = VERTEX_POINT('',#1172); +#1172 = CARTESIAN_POINT('',(19.166666666667,-18.,8.)); +#1173 = LINE('',#1174,#1175); +#1174 = CARTESIAN_POINT('',(-42.91666666666,-18.,8.)); +#1175 = VECTOR('',#1176,1.); +#1176 = DIRECTION('',(1.,0.,-0.)); +#1177 = ORIENTED_EDGE('',*,*,#1178,.F.); +#1178 = EDGE_CURVE('',#1179,#1171,#1181,.T.); +#1179 = VERTEX_POINT('',#1180); +#1180 = CARTESIAN_POINT('',(19.166666666667,-18.,40.)); +#1181 = LINE('',#1182,#1183); +#1182 = CARTESIAN_POINT('',(19.166666666667,-18.,4.)); +#1183 = VECTOR('',#1184,1.); +#1184 = DIRECTION('',(-0.,0.,-1.)); +#1185 = ORIENTED_EDGE('',*,*,#1186,.T.); +#1186 = EDGE_CURVE('',#1179,#1187,#1189,.T.); +#1187 = VERTEX_POINT('',#1188); +#1188 = CARTESIAN_POINT('',(25.277777777778,-18.,40.)); +#1189 = LINE('',#1190,#1191); +#1190 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1191 = VECTOR('',#1192,1.); +#1192 = DIRECTION('',(1.,0.,-0.)); +#1193 = ORIENTED_EDGE('',*,*,#1194,.T.); +#1194 = EDGE_CURVE('',#1187,#1195,#1197,.T.); +#1195 = VERTEX_POINT('',#1196); +#1196 = CARTESIAN_POINT('',(25.277777777778,-18.,8.)); +#1197 = LINE('',#1198,#1199); +#1198 = CARTESIAN_POINT('',(25.277777777778,-18.,4.)); +#1199 = VECTOR('',#1200,1.); +#1200 = DIRECTION('',(-0.,0.,-1.)); +#1201 = ORIENTED_EDGE('',*,*,#1202,.T.); +#1202 = EDGE_CURVE('',#1195,#1203,#1205,.T.); +#1203 = VERTEX_POINT('',#1204); +#1204 = CARTESIAN_POINT('',(30.277777777778,-18.,8.)); +#1205 = LINE('',#1206,#1207); +#1206 = CARTESIAN_POINT('',(-37.36111111111,-18.,8.)); +#1207 = VECTOR('',#1208,1.); +#1208 = DIRECTION('',(1.,0.,-0.)); +#1209 = ORIENTED_EDGE('',*,*,#1210,.F.); +#1210 = EDGE_CURVE('',#1211,#1203,#1213,.T.); +#1211 = VERTEX_POINT('',#1212); +#1212 = CARTESIAN_POINT('',(30.277777777778,-18.,40.)); +#1213 = LINE('',#1214,#1215); +#1214 = CARTESIAN_POINT('',(30.277777777778,-18.,4.)); +#1215 = VECTOR('',#1216,1.); +#1216 = DIRECTION('',(-0.,0.,-1.)); +#1217 = ORIENTED_EDGE('',*,*,#1218,.T.); +#1218 = EDGE_CURVE('',#1211,#1219,#1221,.T.); +#1219 = VERTEX_POINT('',#1220); +#1220 = CARTESIAN_POINT('',(36.388888888889,-18.,40.)); +#1221 = LINE('',#1222,#1223); +#1222 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1223 = VECTOR('',#1224,1.); +#1224 = DIRECTION('',(1.,0.,-0.)); +#1225 = ORIENTED_EDGE('',*,*,#1226,.T.); +#1226 = EDGE_CURVE('',#1219,#1227,#1229,.T.); +#1227 = VERTEX_POINT('',#1228); +#1228 = CARTESIAN_POINT('',(36.388888888889,-18.,8.)); +#1229 = LINE('',#1230,#1231); +#1230 = CARTESIAN_POINT('',(36.388888888889,-18.,4.)); +#1231 = VECTOR('',#1232,1.); +#1232 = DIRECTION('',(-0.,0.,-1.)); +#1233 = ORIENTED_EDGE('',*,*,#1234,.T.); +#1234 = EDGE_CURVE('',#1227,#1235,#1237,.T.); +#1235 = VERTEX_POINT('',#1236); +#1236 = CARTESIAN_POINT('',(41.388888888889,-18.,8.)); +#1237 = LINE('',#1238,#1239); +#1238 = CARTESIAN_POINT('',(-31.80555555555,-18.,8.)); +#1239 = VECTOR('',#1240,1.); +#1240 = DIRECTION('',(1.,0.,-0.)); +#1241 = ORIENTED_EDGE('',*,*,#1242,.F.); +#1242 = EDGE_CURVE('',#1243,#1235,#1245,.T.); +#1243 = VERTEX_POINT('',#1244); +#1244 = CARTESIAN_POINT('',(41.388888888889,-18.,40.)); +#1245 = LINE('',#1246,#1247); +#1246 = CARTESIAN_POINT('',(41.388888888889,-18.,4.)); +#1247 = VECTOR('',#1248,1.); +#1248 = DIRECTION('',(-0.,0.,-1.)); +#1249 = ORIENTED_EDGE('',*,*,#1250,.T.); +#1250 = EDGE_CURVE('',#1243,#1251,#1253,.T.); +#1251 = VERTEX_POINT('',#1252); +#1252 = CARTESIAN_POINT('',(47.5,-18.,40.)); +#1253 = LINE('',#1254,#1255); +#1254 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1255 = VECTOR('',#1256,1.); +#1256 = DIRECTION('',(1.,0.,-0.)); +#1257 = ORIENTED_EDGE('',*,*,#1258,.T.); +#1258 = EDGE_CURVE('',#1251,#1259,#1261,.T.); +#1259 = VERTEX_POINT('',#1260); +#1260 = CARTESIAN_POINT('',(47.5,-18.,8.)); +#1261 = LINE('',#1262,#1263); +#1262 = CARTESIAN_POINT('',(47.5,-18.,4.)); +#1263 = VECTOR('',#1264,1.); +#1264 = DIRECTION('',(-0.,0.,-1.)); +#1265 = ORIENTED_EDGE('',*,*,#1266,.T.); +#1266 = EDGE_CURVE('',#1259,#1267,#1269,.T.); +#1267 = VERTEX_POINT('',#1268); +#1268 = CARTESIAN_POINT('',(52.5,-18.,8.)); +#1269 = LINE('',#1270,#1271); +#1270 = CARTESIAN_POINT('',(-26.25,-18.,8.)); +#1271 = VECTOR('',#1272,1.); +#1272 = DIRECTION('',(1.,0.,-0.)); +#1273 = ORIENTED_EDGE('',*,*,#1274,.F.); +#1274 = EDGE_CURVE('',#1275,#1267,#1277,.T.); +#1275 = VERTEX_POINT('',#1276); +#1276 = CARTESIAN_POINT('',(52.5,-18.,40.)); +#1277 = LINE('',#1278,#1279); +#1278 = CARTESIAN_POINT('',(52.5,-18.,4.)); +#1279 = VECTOR('',#1280,1.); +#1280 = DIRECTION('',(-0.,0.,-1.)); +#1281 = ORIENTED_EDGE('',*,*,#1282,.T.); +#1282 = EDGE_CURVE('',#1275,#1283,#1285,.T.); +#1283 = VERTEX_POINT('',#1284); +#1284 = CARTESIAN_POINT('',(58.611111111111,-18.,40.)); +#1285 = LINE('',#1286,#1287); +#1286 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1287 = VECTOR('',#1288,1.); +#1288 = DIRECTION('',(1.,0.,-0.)); +#1289 = ORIENTED_EDGE('',*,*,#1290,.T.); +#1290 = EDGE_CURVE('',#1283,#1291,#1293,.T.); +#1291 = VERTEX_POINT('',#1292); +#1292 = CARTESIAN_POINT('',(58.611111111111,-18.,8.)); +#1293 = LINE('',#1294,#1295); +#1294 = CARTESIAN_POINT('',(58.611111111111,-18.,4.)); +#1295 = VECTOR('',#1296,1.); +#1296 = DIRECTION('',(-0.,0.,-1.)); +#1297 = ORIENTED_EDGE('',*,*,#1298,.T.); +#1298 = EDGE_CURVE('',#1291,#1299,#1301,.T.); +#1299 = VERTEX_POINT('',#1300); +#1300 = CARTESIAN_POINT('',(63.611111111111,-18.,8.)); +#1301 = LINE('',#1302,#1303); +#1302 = CARTESIAN_POINT('',(-20.69444444444,-18.,8.)); +#1303 = VECTOR('',#1304,1.); +#1304 = DIRECTION('',(1.,0.,-0.)); +#1305 = ORIENTED_EDGE('',*,*,#1306,.F.); +#1306 = EDGE_CURVE('',#1307,#1299,#1309,.T.); +#1307 = VERTEX_POINT('',#1308); +#1308 = CARTESIAN_POINT('',(63.611111111111,-18.,40.)); +#1309 = LINE('',#1310,#1311); +#1310 = CARTESIAN_POINT('',(63.611111111111,-18.,4.)); +#1311 = VECTOR('',#1312,1.); +#1312 = DIRECTION('',(-0.,0.,-1.)); +#1313 = ORIENTED_EDGE('',*,*,#1314,.T.); +#1314 = EDGE_CURVE('',#1307,#1315,#1317,.T.); +#1315 = VERTEX_POINT('',#1316); +#1316 = CARTESIAN_POINT('',(69.722222222222,-18.,40.)); +#1317 = LINE('',#1318,#1319); +#1318 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1319 = VECTOR('',#1320,1.); +#1320 = DIRECTION('',(1.,0.,-0.)); +#1321 = ORIENTED_EDGE('',*,*,#1322,.T.); +#1322 = EDGE_CURVE('',#1315,#1323,#1325,.T.); +#1323 = VERTEX_POINT('',#1324); +#1324 = CARTESIAN_POINT('',(69.722222222222,-18.,8.)); +#1325 = LINE('',#1326,#1327); +#1326 = CARTESIAN_POINT('',(69.722222222222,-18.,4.)); +#1327 = VECTOR('',#1328,1.); +#1328 = DIRECTION('',(-0.,0.,-1.)); +#1329 = ORIENTED_EDGE('',*,*,#1330,.T.); +#1330 = EDGE_CURVE('',#1323,#1331,#1333,.T.); +#1331 = VERTEX_POINT('',#1332); +#1332 = CARTESIAN_POINT('',(74.722222222222,-18.,8.)); +#1333 = LINE('',#1334,#1335); +#1334 = CARTESIAN_POINT('',(-15.13888888888,-18.,8.)); +#1335 = VECTOR('',#1336,1.); +#1336 = DIRECTION('',(1.,0.,-0.)); +#1337 = ORIENTED_EDGE('',*,*,#1338,.F.); +#1338 = EDGE_CURVE('',#1339,#1331,#1341,.T.); +#1339 = VERTEX_POINT('',#1340); +#1340 = CARTESIAN_POINT('',(74.722222222222,-18.,40.)); +#1341 = LINE('',#1342,#1343); +#1342 = CARTESIAN_POINT('',(74.722222222222,-18.,4.)); +#1343 = VECTOR('',#1344,1.); +#1344 = DIRECTION('',(-0.,0.,-1.)); +#1345 = ORIENTED_EDGE('',*,*,#1346,.T.); +#1346 = EDGE_CURVE('',#1339,#1347,#1349,.T.); +#1347 = VERTEX_POINT('',#1348); +#1348 = CARTESIAN_POINT('',(80.833333333333,-18.,40.)); +#1349 = LINE('',#1350,#1351); +#1350 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1351 = VECTOR('',#1352,1.); +#1352 = DIRECTION('',(1.,0.,-0.)); +#1353 = ORIENTED_EDGE('',*,*,#1354,.T.); +#1354 = EDGE_CURVE('',#1347,#1355,#1357,.T.); +#1355 = VERTEX_POINT('',#1356); +#1356 = CARTESIAN_POINT('',(80.833333333333,-18.,8.)); +#1357 = LINE('',#1358,#1359); +#1358 = CARTESIAN_POINT('',(80.833333333333,-18.,4.)); +#1359 = VECTOR('',#1360,1.); +#1360 = DIRECTION('',(-0.,0.,-1.)); +#1361 = ORIENTED_EDGE('',*,*,#1362,.T.); +#1362 = EDGE_CURVE('',#1355,#1363,#1365,.T.); +#1363 = VERTEX_POINT('',#1364); +#1364 = CARTESIAN_POINT('',(85.833333333333,-18.,8.)); +#1365 = LINE('',#1366,#1367); +#1366 = CARTESIAN_POINT('',(-9.583333333333,-18.,8.)); +#1367 = VECTOR('',#1368,1.); +#1368 = DIRECTION('',(1.,0.,-0.)); +#1369 = ORIENTED_EDGE('',*,*,#1370,.F.); +#1370 = EDGE_CURVE('',#1371,#1363,#1373,.T.); +#1371 = VERTEX_POINT('',#1372); +#1372 = CARTESIAN_POINT('',(85.833333333333,-18.,40.)); +#1373 = LINE('',#1374,#1375); +#1374 = CARTESIAN_POINT('',(85.833333333333,-18.,4.)); +#1375 = VECTOR('',#1376,1.); +#1376 = DIRECTION('',(-0.,0.,-1.)); +#1377 = ORIENTED_EDGE('',*,*,#1378,.T.); +#1378 = EDGE_CURVE('',#1371,#1379,#1381,.T.); +#1379 = VERTEX_POINT('',#1380); +#1380 = CARTESIAN_POINT('',(91.944444444444,-18.,40.)); +#1381 = LINE('',#1382,#1383); +#1382 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1383 = VECTOR('',#1384,1.); +#1384 = DIRECTION('',(1.,0.,-0.)); +#1385 = ORIENTED_EDGE('',*,*,#1386,.T.); +#1386 = EDGE_CURVE('',#1379,#1387,#1389,.T.); #1387 = VERTEX_POINT('',#1388); -#1388 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1389 = VERTEX_POINT('',#1390); -#1390 = CARTESIAN_POINT('',(-74.72222222222,-20.1,40.)); -#1391 = LINE('',#1392,#1393); -#1392 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1393 = VECTOR('',#1394,1.); -#1394 = DIRECTION('',(0.,0.,1.)); -#1395 = ORIENTED_EDGE('',*,*,#1396,.T.); -#1396 = EDGE_CURVE('',#1387,#1397,#1399,.T.); -#1397 = VERTEX_POINT('',#1398); -#1398 = CARTESIAN_POINT('',(-74.72222222222,-17.9,8.)); -#1399 = LINE('',#1400,#1401); -#1400 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1401 = VECTOR('',#1402,1.); -#1402 = DIRECTION('',(-0.,1.,0.)); -#1403 = ORIENTED_EDGE('',*,*,#1404,.T.); -#1404 = EDGE_CURVE('',#1397,#1405,#1407,.T.); -#1405 = VERTEX_POINT('',#1406); -#1406 = CARTESIAN_POINT('',(-74.72222222222,-17.9,40.)); -#1407 = LINE('',#1408,#1409); -#1408 = CARTESIAN_POINT('',(-74.72222222222,-17.9,8.)); -#1409 = VECTOR('',#1410,1.); -#1410 = DIRECTION('',(0.,0.,1.)); -#1411 = ORIENTED_EDGE('',*,*,#1412,.F.); -#1412 = EDGE_CURVE('',#1389,#1405,#1413,.T.); -#1413 = LINE('',#1414,#1415); -#1414 = CARTESIAN_POINT('',(-74.72222222222,-20.1,40.)); -#1415 = VECTOR('',#1416,1.); -#1416 = DIRECTION('',(-0.,1.,0.)); -#1417 = PLANE('',#1418); -#1418 = AXIS2_PLACEMENT_3D('',#1419,#1420,#1421); -#1419 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1420 = DIRECTION('',(1.,0.,-0.)); -#1421 = DIRECTION('',(0.,0.,1.)); -#1422 = ADVANCED_FACE('',(#1423),#1457,.T.); -#1423 = FACE_BOUND('',#1424,.T.); -#1424 = EDGE_LOOP('',(#1425,#1435,#1443,#1451)); -#1425 = ORIENTED_EDGE('',*,*,#1426,.F.); -#1426 = EDGE_CURVE('',#1427,#1429,#1431,.T.); +#1388 = CARTESIAN_POINT('',(91.944444444444,-18.,8.)); +#1389 = LINE('',#1390,#1391); +#1390 = CARTESIAN_POINT('',(91.944444444444,-18.,4.)); +#1391 = VECTOR('',#1392,1.); +#1392 = DIRECTION('',(-0.,0.,-1.)); +#1393 = ORIENTED_EDGE('',*,*,#1394,.T.); +#1394 = EDGE_CURVE('',#1387,#1395,#1397,.T.); +#1395 = VERTEX_POINT('',#1396); +#1396 = CARTESIAN_POINT('',(96.944444444444,-18.,8.)); +#1397 = LINE('',#1398,#1399); +#1398 = CARTESIAN_POINT('',(-4.027777777778,-18.,8.)); +#1399 = VECTOR('',#1400,1.); +#1400 = DIRECTION('',(1.,0.,-0.)); +#1401 = ORIENTED_EDGE('',*,*,#1402,.F.); +#1402 = EDGE_CURVE('',#1403,#1395,#1405,.T.); +#1403 = VERTEX_POINT('',#1404); +#1404 = CARTESIAN_POINT('',(96.944444444444,-18.,40.)); +#1405 = LINE('',#1406,#1407); +#1406 = CARTESIAN_POINT('',(96.944444444444,-18.,4.)); +#1407 = VECTOR('',#1408,1.); +#1408 = DIRECTION('',(-0.,0.,-1.)); +#1409 = ORIENTED_EDGE('',*,*,#1410,.T.); +#1410 = EDGE_CURVE('',#1403,#827,#1411,.T.); +#1411 = LINE('',#1412,#1413); +#1412 = CARTESIAN_POINT('',(-100.,-18.,40.)); +#1413 = VECTOR('',#1414,1.); +#1414 = DIRECTION('',(1.,0.,-0.)); +#1415 = PLANE('',#1416); +#1416 = AXIS2_PLACEMENT_3D('',#1417,#1418,#1419); +#1417 = CARTESIAN_POINT('',(-100.,-18.,0.)); +#1418 = DIRECTION('',(-0.,1.,0.)); +#1419 = DIRECTION('',(0.,0.,1.)); +#1420 = ADVANCED_FACE('',(#1421,#1439,#1450,#1461),#1472,.T.); +#1421 = FACE_BOUND('',#1422,.T.); +#1422 = EDGE_LOOP('',(#1423,#1424,#1425,#1433)); +#1423 = ORIENTED_EDGE('',*,*,#140,.F.); +#1424 = ORIENTED_EDGE('',*,*,#834,.F.); +#1425 = ORIENTED_EDGE('',*,*,#1426,.T.); +#1426 = EDGE_CURVE('',#181,#1427,#1429,.T.); #1427 = VERTEX_POINT('',#1428); -#1428 = CARTESIAN_POINT('',(-69.72222222222,-20.1,8.)); -#1429 = VERTEX_POINT('',#1430); -#1430 = CARTESIAN_POINT('',(-69.72222222222,-20.1,40.)); -#1431 = LINE('',#1432,#1433); -#1432 = CARTESIAN_POINT('',(-69.72222222222,-20.1,8.)); -#1433 = VECTOR('',#1434,1.); -#1434 = DIRECTION('',(0.,0.,1.)); -#1435 = ORIENTED_EDGE('',*,*,#1436,.T.); -#1436 = EDGE_CURVE('',#1427,#1437,#1439,.T.); -#1437 = VERTEX_POINT('',#1438); -#1438 = CARTESIAN_POINT('',(-69.72222222222,-17.9,8.)); -#1439 = LINE('',#1440,#1441); -#1440 = CARTESIAN_POINT('',(-69.72222222222,-20.1,8.)); -#1441 = VECTOR('',#1442,1.); -#1442 = DIRECTION('',(-0.,1.,0.)); -#1443 = ORIENTED_EDGE('',*,*,#1444,.T.); -#1444 = EDGE_CURVE('',#1437,#1445,#1447,.T.); -#1445 = VERTEX_POINT('',#1446); -#1446 = CARTESIAN_POINT('',(-69.72222222222,-17.9,40.)); -#1447 = LINE('',#1448,#1449); -#1448 = CARTESIAN_POINT('',(-69.72222222222,-17.9,8.)); -#1449 = VECTOR('',#1450,1.); -#1450 = DIRECTION('',(0.,0.,1.)); -#1451 = ORIENTED_EDGE('',*,*,#1452,.F.); -#1452 = EDGE_CURVE('',#1429,#1445,#1453,.T.); -#1453 = LINE('',#1454,#1455); -#1454 = CARTESIAN_POINT('',(-69.72222222222,-20.1,40.)); -#1455 = VECTOR('',#1456,1.); -#1456 = DIRECTION('',(-0.,1.,0.)); -#1457 = PLANE('',#1458); -#1458 = AXIS2_PLACEMENT_3D('',#1459,#1460,#1461); -#1459 = CARTESIAN_POINT('',(-69.72222222222,-20.1,8.)); +#1428 = CARTESIAN_POINT('',(100.,18.,2.)); +#1429 = LINE('',#1430,#1431); +#1430 = CARTESIAN_POINT('',(100.,-20.,2.)); +#1431 = VECTOR('',#1432,1.); +#1432 = DIRECTION('',(-0.,1.,0.)); +#1433 = ORIENTED_EDGE('',*,*,#1434,.T.); +#1434 = EDGE_CURVE('',#1427,#133,#1435,.T.); +#1435 = LINE('',#1436,#1437); +#1436 = CARTESIAN_POINT('',(-100.,18.,2.)); +#1437 = VECTOR('',#1438,1.); +#1438 = DIRECTION('',(-1.,-0.,0.)); +#1439 = FACE_BOUND('',#1440,.T.); +#1440 = EDGE_LOOP('',(#1441)); +#1441 = ORIENTED_EDGE('',*,*,#1442,.F.); +#1442 = EDGE_CURVE('',#1443,#1443,#1445,.T.); +#1443 = VERTEX_POINT('',#1444); +#1444 = CARTESIAN_POINT('',(-57.8,-5.388445916248E-16,2.)); +#1445 = CIRCLE('',#1446,2.2); +#1446 = AXIS2_PLACEMENT_3D('',#1447,#1448,#1449); +#1447 = CARTESIAN_POINT('',(-60.,0.,2.)); +#1448 = DIRECTION('',(0.,0.,1.)); +#1449 = DIRECTION('',(1.,0.,-0.)); +#1450 = FACE_BOUND('',#1451,.T.); +#1451 = EDGE_LOOP('',(#1452)); +#1452 = ORIENTED_EDGE('',*,*,#1453,.F.); +#1453 = EDGE_CURVE('',#1454,#1454,#1456,.T.); +#1454 = VERTEX_POINT('',#1455); +#1455 = CARTESIAN_POINT('',(2.2,-5.388445916248E-16,2.)); +#1456 = CIRCLE('',#1457,2.2); +#1457 = AXIS2_PLACEMENT_3D('',#1458,#1459,#1460); +#1458 = CARTESIAN_POINT('',(0.,0.,2.)); +#1459 = DIRECTION('',(0.,0.,1.)); #1460 = DIRECTION('',(1.,0.,-0.)); -#1461 = DIRECTION('',(0.,0.,1.)); -#1462 = ADVANCED_FACE('',(#1463),#1479,.F.); -#1463 = FACE_BOUND('',#1464,.F.); -#1464 = EDGE_LOOP('',(#1465,#1471,#1472,#1478)); -#1465 = ORIENTED_EDGE('',*,*,#1466,.F.); -#1466 = EDGE_CURVE('',#1387,#1427,#1467,.T.); -#1467 = LINE('',#1468,#1469); -#1468 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1469 = VECTOR('',#1470,1.); -#1470 = DIRECTION('',(1.,0.,-0.)); -#1471 = ORIENTED_EDGE('',*,*,#1386,.T.); -#1472 = ORIENTED_EDGE('',*,*,#1473,.T.); -#1473 = EDGE_CURVE('',#1389,#1429,#1474,.T.); -#1474 = LINE('',#1475,#1476); -#1475 = CARTESIAN_POINT('',(-74.72222222222,-20.1,40.)); -#1476 = VECTOR('',#1477,1.); -#1477 = DIRECTION('',(1.,0.,-0.)); -#1478 = ORIENTED_EDGE('',*,*,#1426,.F.); -#1479 = PLANE('',#1480); -#1480 = AXIS2_PLACEMENT_3D('',#1481,#1482,#1483); -#1481 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1482 = DIRECTION('',(-0.,1.,0.)); -#1483 = DIRECTION('',(0.,0.,1.)); -#1484 = ADVANCED_FACE('',(#1485),#1501,.T.); -#1485 = FACE_BOUND('',#1486,.T.); -#1486 = EDGE_LOOP('',(#1487,#1493,#1494,#1500)); -#1487 = ORIENTED_EDGE('',*,*,#1488,.F.); -#1488 = EDGE_CURVE('',#1397,#1437,#1489,.T.); -#1489 = LINE('',#1490,#1491); -#1490 = CARTESIAN_POINT('',(-74.72222222222,-17.9,8.)); -#1491 = VECTOR('',#1492,1.); -#1492 = DIRECTION('',(1.,0.,-0.)); -#1493 = ORIENTED_EDGE('',*,*,#1404,.T.); -#1494 = ORIENTED_EDGE('',*,*,#1495,.T.); -#1495 = EDGE_CURVE('',#1405,#1445,#1496,.T.); -#1496 = LINE('',#1497,#1498); -#1497 = CARTESIAN_POINT('',(-74.72222222222,-17.9,40.)); -#1498 = VECTOR('',#1499,1.); -#1499 = DIRECTION('',(1.,0.,-0.)); -#1500 = ORIENTED_EDGE('',*,*,#1444,.F.); -#1501 = PLANE('',#1502); -#1502 = AXIS2_PLACEMENT_3D('',#1503,#1504,#1505); -#1503 = CARTESIAN_POINT('',(-74.72222222222,-17.9,8.)); -#1504 = DIRECTION('',(-0.,1.,0.)); -#1505 = DIRECTION('',(0.,0.,1.)); -#1506 = ADVANCED_FACE('',(#1507),#1513,.F.); -#1507 = FACE_BOUND('',#1508,.F.); -#1508 = EDGE_LOOP('',(#1509,#1510,#1511,#1512)); -#1509 = ORIENTED_EDGE('',*,*,#1396,.F.); -#1510 = ORIENTED_EDGE('',*,*,#1466,.T.); -#1511 = ORIENTED_EDGE('',*,*,#1436,.T.); -#1512 = ORIENTED_EDGE('',*,*,#1488,.F.); -#1513 = PLANE('',#1514); -#1514 = AXIS2_PLACEMENT_3D('',#1515,#1516,#1517); -#1515 = CARTESIAN_POINT('',(-74.72222222222,-20.1,8.)); -#1516 = DIRECTION('',(0.,0.,1.)); -#1517 = DIRECTION('',(1.,0.,-0.)); -#1518 = ADVANCED_FACE('',(#1519),#1525,.T.); -#1519 = FACE_BOUND('',#1520,.T.); -#1520 = EDGE_LOOP('',(#1521,#1522,#1523,#1524)); -#1521 = ORIENTED_EDGE('',*,*,#1412,.F.); -#1522 = ORIENTED_EDGE('',*,*,#1473,.T.); -#1523 = ORIENTED_EDGE('',*,*,#1452,.T.); -#1524 = ORIENTED_EDGE('',*,*,#1495,.F.); -#1525 = PLANE('',#1526); -#1526 = AXIS2_PLACEMENT_3D('',#1527,#1528,#1529); -#1527 = CARTESIAN_POINT('',(-74.72222222222,-20.1,40.)); -#1528 = DIRECTION('',(0.,0.,1.)); -#1529 = DIRECTION('',(1.,0.,-0.)); -#1530 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1534)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1531,#1532,#1533)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1531 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1532 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1533 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1534 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1531, - 'distance_accuracy_value','confusion accuracy'); -#1535 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1536,#1538); -#1536 = ( REPRESENTATION_RELATIONSHIP('','',#1379,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1537) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#1537 = ITEM_DEFINED_TRANSFORMATION('','',#11,#43); -#1538 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #1539); -#1539 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('8','WireDuct_LeftCombSlot_03','' - ,#5,#1374,$); -#1540 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1376)); -#1541 = SHAPE_DEFINITION_REPRESENTATION(#1542,#1548); -#1542 = PRODUCT_DEFINITION_SHAPE('','',#1543); -#1543 = PRODUCT_DEFINITION('design','',#1544,#1547); -#1544 = PRODUCT_DEFINITION_FORMATION('','',#1545); -#1545 = PRODUCT('WireDuct_RightCombSlot_03','WireDuct_RightCombSlot_03', - '',(#1546)); -#1546 = PRODUCT_CONTEXT('',#2,'mechanical'); -#1547 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#1548 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1549),#1699); -#1549 = MANIFOLD_SOLID_BREP('',#1550); -#1550 = CLOSED_SHELL('',(#1551,#1591,#1631,#1653,#1675,#1687)); -#1551 = ADVANCED_FACE('',(#1552),#1586,.F.); -#1552 = FACE_BOUND('',#1553,.F.); -#1553 = EDGE_LOOP('',(#1554,#1564,#1572,#1580)); -#1554 = ORIENTED_EDGE('',*,*,#1555,.F.); -#1555 = EDGE_CURVE('',#1556,#1558,#1560,.T.); -#1556 = VERTEX_POINT('',#1557); -#1557 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1558 = VERTEX_POINT('',#1559); -#1559 = CARTESIAN_POINT('',(-74.72222222222,17.9,40.)); -#1560 = LINE('',#1561,#1562); -#1561 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1562 = VECTOR('',#1563,1.); +#1461 = FACE_BOUND('',#1462,.T.); +#1462 = EDGE_LOOP('',(#1463)); +#1463 = ORIENTED_EDGE('',*,*,#1464,.F.); +#1464 = EDGE_CURVE('',#1465,#1465,#1467,.T.); +#1465 = VERTEX_POINT('',#1466); +#1466 = CARTESIAN_POINT('',(62.2,-5.388445916248E-16,2.)); +#1467 = CIRCLE('',#1468,2.2); +#1468 = AXIS2_PLACEMENT_3D('',#1469,#1470,#1471); +#1469 = CARTESIAN_POINT('',(60.,0.,2.)); +#1470 = DIRECTION('',(0.,0.,1.)); +#1471 = DIRECTION('',(1.,0.,-0.)); +#1472 = PLANE('',#1473); +#1473 = AXIS2_PLACEMENT_3D('',#1474,#1475,#1476); +#1474 = CARTESIAN_POINT('',(-100.,-20.,2.)); +#1475 = DIRECTION('',(0.,0.,1.)); +#1476 = DIRECTION('',(1.,0.,-0.)); +#1477 = ADVANCED_FACE('',(#1478),#1503,.F.); +#1478 = FACE_BOUND('',#1479,.F.); +#1479 = EDGE_LOOP('',(#1480,#1481,#1489,#1497)); +#1480 = ORIENTED_EDGE('',*,*,#132,.F.); +#1481 = ORIENTED_EDGE('',*,*,#1482,.T.); +#1482 = EDGE_CURVE('',#125,#1483,#1485,.T.); +#1483 = VERTEX_POINT('',#1484); +#1484 = CARTESIAN_POINT('',(-100.,20.,0.)); +#1485 = LINE('',#1486,#1487); +#1486 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#1487 = VECTOR('',#1488,1.); +#1488 = DIRECTION('',(-0.,1.,0.)); +#1489 = ORIENTED_EDGE('',*,*,#1490,.T.); +#1490 = EDGE_CURVE('',#1483,#1491,#1493,.T.); +#1491 = VERTEX_POINT('',#1492); +#1492 = CARTESIAN_POINT('',(-100.,20.,2.)); +#1493 = LINE('',#1494,#1495); +#1494 = CARTESIAN_POINT('',(-100.,20.,0.)); +#1495 = VECTOR('',#1496,1.); +#1496 = DIRECTION('',(0.,0.,1.)); +#1497 = ORIENTED_EDGE('',*,*,#1498,.F.); +#1498 = EDGE_CURVE('',#133,#1491,#1499,.T.); +#1499 = LINE('',#1500,#1501); +#1500 = CARTESIAN_POINT('',(-100.,-20.,2.)); +#1501 = VECTOR('',#1502,1.); +#1502 = DIRECTION('',(-0.,1.,0.)); +#1503 = PLANE('',#1504); +#1504 = AXIS2_PLACEMENT_3D('',#1505,#1506,#1507); +#1505 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#1506 = DIRECTION('',(1.,0.,-0.)); +#1507 = DIRECTION('',(0.,0.,1.)); +#1508 = ADVANCED_FACE('',(#1509,#1527,#1538,#1549),#1560,.F.); +#1509 = FACE_BOUND('',#1510,.F.); +#1510 = EDGE_LOOP('',(#1511,#1512,#1513,#1521)); +#1511 = ORIENTED_EDGE('',*,*,#124,.F.); +#1512 = ORIENTED_EDGE('',*,*,#164,.T.); +#1513 = ORIENTED_EDGE('',*,*,#1514,.T.); +#1514 = EDGE_CURVE('',#157,#1515,#1517,.T.); +#1515 = VERTEX_POINT('',#1516); +#1516 = CARTESIAN_POINT('',(100.,18.,0.)); +#1517 = LINE('',#1518,#1519); +#1518 = CARTESIAN_POINT('',(100.,-20.,0.)); +#1519 = VECTOR('',#1520,1.); +#1520 = DIRECTION('',(-0.,1.,0.)); +#1521 = ORIENTED_EDGE('',*,*,#1522,.F.); +#1522 = EDGE_CURVE('',#125,#1515,#1523,.T.); +#1523 = LINE('',#1524,#1525); +#1524 = CARTESIAN_POINT('',(-100.,18.,0.)); +#1525 = VECTOR('',#1526,1.); +#1526 = DIRECTION('',(1.,0.,-0.)); +#1527 = FACE_BOUND('',#1528,.F.); +#1528 = EDGE_LOOP('',(#1529)); +#1529 = ORIENTED_EDGE('',*,*,#1530,.F.); +#1530 = EDGE_CURVE('',#1531,#1531,#1533,.T.); +#1531 = VERTEX_POINT('',#1532); +#1532 = CARTESIAN_POINT('',(-57.8,-5.388445916248E-16,0.)); +#1533 = CIRCLE('',#1534,2.2); +#1534 = AXIS2_PLACEMENT_3D('',#1535,#1536,#1537); +#1535 = CARTESIAN_POINT('',(-60.,0.,0.)); +#1536 = DIRECTION('',(0.,0.,1.)); +#1537 = DIRECTION('',(1.,0.,-0.)); +#1538 = FACE_BOUND('',#1539,.F.); +#1539 = EDGE_LOOP('',(#1540)); +#1540 = ORIENTED_EDGE('',*,*,#1541,.F.); +#1541 = EDGE_CURVE('',#1542,#1542,#1544,.T.); +#1542 = VERTEX_POINT('',#1543); +#1543 = CARTESIAN_POINT('',(2.2,-5.388445916248E-16,0.)); +#1544 = CIRCLE('',#1545,2.2); +#1545 = AXIS2_PLACEMENT_3D('',#1546,#1547,#1548); +#1546 = CARTESIAN_POINT('',(0.,0.,0.)); +#1547 = DIRECTION('',(0.,0.,1.)); +#1548 = DIRECTION('',(1.,0.,-0.)); +#1549 = FACE_BOUND('',#1550,.F.); +#1550 = EDGE_LOOP('',(#1551)); +#1551 = ORIENTED_EDGE('',*,*,#1552,.F.); +#1552 = EDGE_CURVE('',#1553,#1553,#1555,.T.); +#1553 = VERTEX_POINT('',#1554); +#1554 = CARTESIAN_POINT('',(62.2,-5.388445916248E-16,0.)); +#1555 = CIRCLE('',#1556,2.2); +#1556 = AXIS2_PLACEMENT_3D('',#1557,#1558,#1559); +#1557 = CARTESIAN_POINT('',(60.,0.,0.)); +#1558 = DIRECTION('',(0.,0.,1.)); +#1559 = DIRECTION('',(1.,0.,-0.)); +#1560 = PLANE('',#1561); +#1561 = AXIS2_PLACEMENT_3D('',#1562,#1563,#1564); +#1562 = CARTESIAN_POINT('',(-100.,-20.,0.)); #1563 = DIRECTION('',(0.,0.,1.)); -#1564 = ORIENTED_EDGE('',*,*,#1565,.T.); -#1565 = EDGE_CURVE('',#1556,#1566,#1568,.T.); -#1566 = VERTEX_POINT('',#1567); -#1567 = CARTESIAN_POINT('',(-74.72222222222,20.1,8.)); -#1568 = LINE('',#1569,#1570); -#1569 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1570 = VECTOR('',#1571,1.); -#1571 = DIRECTION('',(-0.,1.,0.)); -#1572 = ORIENTED_EDGE('',*,*,#1573,.T.); -#1573 = EDGE_CURVE('',#1566,#1574,#1576,.T.); -#1574 = VERTEX_POINT('',#1575); -#1575 = CARTESIAN_POINT('',(-74.72222222222,20.1,40.)); -#1576 = LINE('',#1577,#1578); -#1577 = CARTESIAN_POINT('',(-74.72222222222,20.1,8.)); -#1578 = VECTOR('',#1579,1.); -#1579 = DIRECTION('',(0.,0.,1.)); -#1580 = ORIENTED_EDGE('',*,*,#1581,.F.); -#1581 = EDGE_CURVE('',#1558,#1574,#1582,.T.); -#1582 = LINE('',#1583,#1584); -#1583 = CARTESIAN_POINT('',(-74.72222222222,17.9,40.)); -#1584 = VECTOR('',#1585,1.); -#1585 = DIRECTION('',(-0.,1.,0.)); -#1586 = PLANE('',#1587); -#1587 = AXIS2_PLACEMENT_3D('',#1588,#1589,#1590); -#1588 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1589 = DIRECTION('',(1.,0.,-0.)); -#1590 = DIRECTION('',(0.,0.,1.)); -#1591 = ADVANCED_FACE('',(#1592),#1626,.T.); -#1592 = FACE_BOUND('',#1593,.T.); -#1593 = EDGE_LOOP('',(#1594,#1604,#1612,#1620)); -#1594 = ORIENTED_EDGE('',*,*,#1595,.F.); -#1595 = EDGE_CURVE('',#1596,#1598,#1600,.T.); -#1596 = VERTEX_POINT('',#1597); -#1597 = CARTESIAN_POINT('',(-69.72222222222,17.9,8.)); -#1598 = VERTEX_POINT('',#1599); -#1599 = CARTESIAN_POINT('',(-69.72222222222,17.9,40.)); -#1600 = LINE('',#1601,#1602); -#1601 = CARTESIAN_POINT('',(-69.72222222222,17.9,8.)); -#1602 = VECTOR('',#1603,1.); -#1603 = DIRECTION('',(0.,0.,1.)); -#1604 = ORIENTED_EDGE('',*,*,#1605,.T.); -#1605 = EDGE_CURVE('',#1596,#1606,#1608,.T.); -#1606 = VERTEX_POINT('',#1607); -#1607 = CARTESIAN_POINT('',(-69.72222222222,20.1,8.)); -#1608 = LINE('',#1609,#1610); -#1609 = CARTESIAN_POINT('',(-69.72222222222,17.9,8.)); -#1610 = VECTOR('',#1611,1.); -#1611 = DIRECTION('',(-0.,1.,0.)); -#1612 = ORIENTED_EDGE('',*,*,#1613,.T.); -#1613 = EDGE_CURVE('',#1606,#1614,#1616,.T.); -#1614 = VERTEX_POINT('',#1615); -#1615 = CARTESIAN_POINT('',(-69.72222222222,20.1,40.)); -#1616 = LINE('',#1617,#1618); -#1617 = CARTESIAN_POINT('',(-69.72222222222,20.1,8.)); -#1618 = VECTOR('',#1619,1.); -#1619 = DIRECTION('',(0.,0.,1.)); -#1620 = ORIENTED_EDGE('',*,*,#1621,.F.); -#1621 = EDGE_CURVE('',#1598,#1614,#1622,.T.); -#1622 = LINE('',#1623,#1624); -#1623 = CARTESIAN_POINT('',(-69.72222222222,17.9,40.)); -#1624 = VECTOR('',#1625,1.); -#1625 = DIRECTION('',(-0.,1.,0.)); -#1626 = PLANE('',#1627); -#1627 = AXIS2_PLACEMENT_3D('',#1628,#1629,#1630); -#1628 = CARTESIAN_POINT('',(-69.72222222222,17.9,8.)); -#1629 = DIRECTION('',(1.,0.,-0.)); -#1630 = DIRECTION('',(0.,0.,1.)); -#1631 = ADVANCED_FACE('',(#1632),#1648,.F.); -#1632 = FACE_BOUND('',#1633,.F.); -#1633 = EDGE_LOOP('',(#1634,#1640,#1641,#1647)); -#1634 = ORIENTED_EDGE('',*,*,#1635,.F.); -#1635 = EDGE_CURVE('',#1556,#1596,#1636,.T.); -#1636 = LINE('',#1637,#1638); -#1637 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1638 = VECTOR('',#1639,1.); -#1639 = DIRECTION('',(1.,0.,-0.)); -#1640 = ORIENTED_EDGE('',*,*,#1555,.T.); -#1641 = ORIENTED_EDGE('',*,*,#1642,.T.); -#1642 = EDGE_CURVE('',#1558,#1598,#1643,.T.); -#1643 = LINE('',#1644,#1645); -#1644 = CARTESIAN_POINT('',(-74.72222222222,17.9,40.)); -#1645 = VECTOR('',#1646,1.); -#1646 = DIRECTION('',(1.,0.,-0.)); -#1647 = ORIENTED_EDGE('',*,*,#1595,.F.); -#1648 = PLANE('',#1649); -#1649 = AXIS2_PLACEMENT_3D('',#1650,#1651,#1652); -#1650 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1651 = DIRECTION('',(-0.,1.,0.)); -#1652 = DIRECTION('',(0.,0.,1.)); -#1653 = ADVANCED_FACE('',(#1654),#1670,.T.); -#1654 = FACE_BOUND('',#1655,.T.); -#1655 = EDGE_LOOP('',(#1656,#1662,#1663,#1669)); -#1656 = ORIENTED_EDGE('',*,*,#1657,.F.); -#1657 = EDGE_CURVE('',#1566,#1606,#1658,.T.); -#1658 = LINE('',#1659,#1660); -#1659 = CARTESIAN_POINT('',(-74.72222222222,20.1,8.)); -#1660 = VECTOR('',#1661,1.); -#1661 = DIRECTION('',(1.,0.,-0.)); -#1662 = ORIENTED_EDGE('',*,*,#1573,.T.); -#1663 = ORIENTED_EDGE('',*,*,#1664,.T.); -#1664 = EDGE_CURVE('',#1574,#1614,#1665,.T.); -#1665 = LINE('',#1666,#1667); -#1666 = CARTESIAN_POINT('',(-74.72222222222,20.1,40.)); -#1667 = VECTOR('',#1668,1.); -#1668 = DIRECTION('',(1.,0.,-0.)); -#1669 = ORIENTED_EDGE('',*,*,#1613,.F.); -#1670 = PLANE('',#1671); -#1671 = AXIS2_PLACEMENT_3D('',#1672,#1673,#1674); -#1672 = CARTESIAN_POINT('',(-74.72222222222,20.1,8.)); -#1673 = DIRECTION('',(-0.,1.,0.)); -#1674 = DIRECTION('',(0.,0.,1.)); -#1675 = ADVANCED_FACE('',(#1676),#1682,.F.); -#1676 = FACE_BOUND('',#1677,.F.); -#1677 = EDGE_LOOP('',(#1678,#1679,#1680,#1681)); -#1678 = ORIENTED_EDGE('',*,*,#1565,.F.); -#1679 = ORIENTED_EDGE('',*,*,#1635,.T.); -#1680 = ORIENTED_EDGE('',*,*,#1605,.T.); -#1681 = ORIENTED_EDGE('',*,*,#1657,.F.); -#1682 = PLANE('',#1683); -#1683 = AXIS2_PLACEMENT_3D('',#1684,#1685,#1686); -#1684 = CARTESIAN_POINT('',(-74.72222222222,17.9,8.)); -#1685 = DIRECTION('',(0.,0.,1.)); -#1686 = DIRECTION('',(1.,0.,-0.)); -#1687 = ADVANCED_FACE('',(#1688),#1694,.T.); -#1688 = FACE_BOUND('',#1689,.T.); -#1689 = EDGE_LOOP('',(#1690,#1691,#1692,#1693)); -#1690 = ORIENTED_EDGE('',*,*,#1581,.F.); -#1691 = ORIENTED_EDGE('',*,*,#1642,.T.); -#1692 = ORIENTED_EDGE('',*,*,#1621,.T.); -#1693 = ORIENTED_EDGE('',*,*,#1664,.F.); -#1694 = PLANE('',#1695); -#1695 = AXIS2_PLACEMENT_3D('',#1696,#1697,#1698); -#1696 = CARTESIAN_POINT('',(-74.72222222222,17.9,40.)); -#1697 = DIRECTION('',(0.,0.,1.)); -#1698 = DIRECTION('',(1.,0.,-0.)); -#1699 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1703)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1700,#1701,#1702)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1700 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1701 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1702 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1703 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1700, - 'distance_accuracy_value','confusion accuracy'); -#1704 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1705,#1707); -#1705 = ( REPRESENTATION_RELATIONSHIP('','',#1548,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1706) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#1706 = ITEM_DEFINED_TRANSFORMATION('','',#11,#47); -#1707 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #1708); -#1708 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('9','WireDuct_RightCombSlot_03', - '',#5,#1543,$); -#1709 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1545)); -#1710 = SHAPE_DEFINITION_REPRESENTATION(#1711,#1717); -#1711 = PRODUCT_DEFINITION_SHAPE('','',#1712); -#1712 = PRODUCT_DEFINITION('design','',#1713,#1716); -#1713 = PRODUCT_DEFINITION_FORMATION('','',#1714); -#1714 = PRODUCT('WireDuct_LeftCombSlot_04','WireDuct_LeftCombSlot_04','' - ,(#1715)); -#1715 = PRODUCT_CONTEXT('',#2,'mechanical'); -#1716 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#1717 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1718),#1868); -#1718 = MANIFOLD_SOLID_BREP('',#1719); -#1719 = CLOSED_SHELL('',(#1720,#1760,#1800,#1822,#1844,#1856)); -#1720 = ADVANCED_FACE('',(#1721),#1755,.F.); -#1721 = FACE_BOUND('',#1722,.F.); -#1722 = EDGE_LOOP('',(#1723,#1733,#1741,#1749)); -#1723 = ORIENTED_EDGE('',*,*,#1724,.F.); -#1724 = EDGE_CURVE('',#1725,#1727,#1729,.T.); -#1725 = VERTEX_POINT('',#1726); -#1726 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1727 = VERTEX_POINT('',#1728); -#1728 = CARTESIAN_POINT('',(-63.61111111111,-20.1,40.)); -#1729 = LINE('',#1730,#1731); -#1730 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1731 = VECTOR('',#1732,1.); -#1732 = DIRECTION('',(0.,0.,1.)); -#1733 = ORIENTED_EDGE('',*,*,#1734,.T.); -#1734 = EDGE_CURVE('',#1725,#1735,#1737,.T.); -#1735 = VERTEX_POINT('',#1736); -#1736 = CARTESIAN_POINT('',(-63.61111111111,-17.9,8.)); -#1737 = LINE('',#1738,#1739); -#1738 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1739 = VECTOR('',#1740,1.); -#1740 = DIRECTION('',(-0.,1.,0.)); -#1741 = ORIENTED_EDGE('',*,*,#1742,.T.); -#1742 = EDGE_CURVE('',#1735,#1743,#1745,.T.); -#1743 = VERTEX_POINT('',#1744); -#1744 = CARTESIAN_POINT('',(-63.61111111111,-17.9,40.)); -#1745 = LINE('',#1746,#1747); -#1746 = CARTESIAN_POINT('',(-63.61111111111,-17.9,8.)); -#1747 = VECTOR('',#1748,1.); -#1748 = DIRECTION('',(0.,0.,1.)); -#1749 = ORIENTED_EDGE('',*,*,#1750,.F.); -#1750 = EDGE_CURVE('',#1727,#1743,#1751,.T.); -#1751 = LINE('',#1752,#1753); -#1752 = CARTESIAN_POINT('',(-63.61111111111,-20.1,40.)); -#1753 = VECTOR('',#1754,1.); -#1754 = DIRECTION('',(-0.,1.,0.)); -#1755 = PLANE('',#1756); -#1756 = AXIS2_PLACEMENT_3D('',#1757,#1758,#1759); -#1757 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1758 = DIRECTION('',(1.,0.,-0.)); -#1759 = DIRECTION('',(0.,0.,1.)); -#1760 = ADVANCED_FACE('',(#1761),#1795,.T.); -#1761 = FACE_BOUND('',#1762,.T.); -#1762 = EDGE_LOOP('',(#1763,#1773,#1781,#1789)); -#1763 = ORIENTED_EDGE('',*,*,#1764,.F.); -#1764 = EDGE_CURVE('',#1765,#1767,#1769,.T.); -#1765 = VERTEX_POINT('',#1766); -#1766 = CARTESIAN_POINT('',(-58.61111111111,-20.1,8.)); -#1767 = VERTEX_POINT('',#1768); -#1768 = CARTESIAN_POINT('',(-58.61111111111,-20.1,40.)); -#1769 = LINE('',#1770,#1771); -#1770 = CARTESIAN_POINT('',(-58.61111111111,-20.1,8.)); -#1771 = VECTOR('',#1772,1.); -#1772 = DIRECTION('',(0.,0.,1.)); -#1773 = ORIENTED_EDGE('',*,*,#1774,.T.); -#1774 = EDGE_CURVE('',#1765,#1775,#1777,.T.); -#1775 = VERTEX_POINT('',#1776); -#1776 = CARTESIAN_POINT('',(-58.61111111111,-17.9,8.)); -#1777 = LINE('',#1778,#1779); -#1778 = CARTESIAN_POINT('',(-58.61111111111,-20.1,8.)); -#1779 = VECTOR('',#1780,1.); -#1780 = DIRECTION('',(-0.,1.,0.)); -#1781 = ORIENTED_EDGE('',*,*,#1782,.T.); -#1782 = EDGE_CURVE('',#1775,#1783,#1785,.T.); -#1783 = VERTEX_POINT('',#1784); -#1784 = CARTESIAN_POINT('',(-58.61111111111,-17.9,40.)); -#1785 = LINE('',#1786,#1787); -#1786 = CARTESIAN_POINT('',(-58.61111111111,-17.9,8.)); -#1787 = VECTOR('',#1788,1.); -#1788 = DIRECTION('',(0.,0.,1.)); +#1564 = DIRECTION('',(1.,0.,-0.)); +#1565 = ADVANCED_FACE('',(#1566),#1577,.T.); +#1566 = FACE_BOUND('',#1567,.T.); +#1567 = EDGE_LOOP('',(#1568,#1569,#1570,#1571)); +#1568 = ORIENTED_EDGE('',*,*,#788,.F.); +#1569 = ORIENTED_EDGE('',*,*,#188,.T.); +#1570 = ORIENTED_EDGE('',*,*,#826,.T.); +#1571 = ORIENTED_EDGE('',*,*,#1572,.F.); +#1572 = EDGE_CURVE('',#781,#827,#1573,.T.); +#1573 = LINE('',#1574,#1575); +#1574 = CARTESIAN_POINT('',(100.,-20.,40.)); +#1575 = VECTOR('',#1576,1.); +#1576 = DIRECTION('',(-0.,1.,0.)); +#1577 = PLANE('',#1578); +#1578 = AXIS2_PLACEMENT_3D('',#1579,#1580,#1581); +#1579 = CARTESIAN_POINT('',(100.,-20.,0.)); +#1580 = DIRECTION('',(1.,0.,-0.)); +#1581 = DIRECTION('',(0.,0.,1.)); +#1582 = ADVANCED_FACE('',(#1583),#1594,.T.); +#1583 = FACE_BOUND('',#1584,.T.); +#1584 = EDGE_LOOP('',(#1585,#1586,#1587,#1593)); +#1585 = ORIENTED_EDGE('',*,*,#180,.F.); +#1586 = ORIENTED_EDGE('',*,*,#1514,.T.); +#1587 = ORIENTED_EDGE('',*,*,#1588,.T.); +#1588 = EDGE_CURVE('',#1515,#1427,#1589,.T.); +#1589 = LINE('',#1590,#1591); +#1590 = CARTESIAN_POINT('',(100.,18.,0.)); +#1591 = VECTOR('',#1592,1.); +#1592 = DIRECTION('',(0.,0.,1.)); +#1593 = ORIENTED_EDGE('',*,*,#1426,.F.); +#1594 = PLANE('',#1595); +#1595 = AXIS2_PLACEMENT_3D('',#1596,#1597,#1598); +#1596 = CARTESIAN_POINT('',(100.,-20.,0.)); +#1597 = DIRECTION('',(1.,0.,-0.)); +#1598 = DIRECTION('',(0.,0.,1.)); +#1599 = ADVANCED_FACE('',(#1600),#1611,.T.); +#1600 = FACE_BOUND('',#1601,.T.); +#1601 = EDGE_LOOP('',(#1602,#1603,#1609,#1610)); +#1602 = ORIENTED_EDGE('',*,*,#1410,.F.); +#1603 = ORIENTED_EDGE('',*,*,#1604,.F.); +#1604 = EDGE_CURVE('',#773,#1403,#1605,.T.); +#1605 = LINE('',#1606,#1607); +#1606 = CARTESIAN_POINT('',(96.944444444444,-20.2,40.)); +#1607 = VECTOR('',#1608,1.); +#1608 = DIRECTION('',(-0.,1.,0.)); +#1609 = ORIENTED_EDGE('',*,*,#780,.T.); +#1610 = ORIENTED_EDGE('',*,*,#1572,.T.); +#1611 = PLANE('',#1612); +#1612 = AXIS2_PLACEMENT_3D('',#1613,#1614,#1615); +#1613 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#1614 = DIRECTION('',(0.,0.,1.)); +#1615 = DIRECTION('',(1.,0.,-0.)); +#1616 = ADVANCED_FACE('',(#1617),#1628,.F.); +#1617 = FACE_BOUND('',#1618,.F.); +#1618 = EDGE_LOOP('',(#1619,#1620,#1621,#1627)); +#1619 = ORIENTED_EDGE('',*,*,#1604,.F.); +#1620 = ORIENTED_EDGE('',*,*,#772,.T.); +#1621 = ORIENTED_EDGE('',*,*,#1622,.T.); +#1622 = EDGE_CURVE('',#765,#1395,#1623,.T.); +#1623 = LINE('',#1624,#1625); +#1624 = CARTESIAN_POINT('',(96.944444444444,-20.2,8.)); +#1625 = VECTOR('',#1626,1.); +#1626 = DIRECTION('',(-0.,1.,0.)); +#1627 = ORIENTED_EDGE('',*,*,#1402,.F.); +#1628 = PLANE('',#1629); +#1629 = AXIS2_PLACEMENT_3D('',#1630,#1631,#1632); +#1630 = CARTESIAN_POINT('',(96.944444444444,-20.2,8.)); +#1631 = DIRECTION('',(1.,0.,-0.)); +#1632 = DIRECTION('',(0.,0.,1.)); +#1633 = ADVANCED_FACE('',(#1634),#1645,.T.); +#1634 = FACE_BOUND('',#1635,.T.); +#1635 = EDGE_LOOP('',(#1636,#1642,#1643,#1644)); +#1636 = ORIENTED_EDGE('',*,*,#1637,.F.); +#1637 = EDGE_CURVE('',#757,#1387,#1638,.T.); +#1638 = LINE('',#1639,#1640); +#1639 = CARTESIAN_POINT('',(91.944444444444,-20.2,8.)); +#1640 = VECTOR('',#1641,1.); +#1641 = DIRECTION('',(-0.,1.,0.)); +#1642 = ORIENTED_EDGE('',*,*,#764,.T.); +#1643 = ORIENTED_EDGE('',*,*,#1622,.T.); +#1644 = ORIENTED_EDGE('',*,*,#1394,.F.); +#1645 = PLANE('',#1646); +#1646 = AXIS2_PLACEMENT_3D('',#1647,#1648,#1649); +#1647 = CARTESIAN_POINT('',(91.944444444444,-20.2,8.)); +#1648 = DIRECTION('',(0.,0.,1.)); +#1649 = DIRECTION('',(1.,0.,-0.)); +#1650 = ADVANCED_FACE('',(#1651),#1662,.T.); +#1651 = FACE_BOUND('',#1652,.T.); +#1652 = EDGE_LOOP('',(#1653,#1659,#1660,#1661)); +#1653 = ORIENTED_EDGE('',*,*,#1654,.F.); +#1654 = EDGE_CURVE('',#749,#1379,#1655,.T.); +#1655 = LINE('',#1656,#1657); +#1656 = CARTESIAN_POINT('',(91.944444444444,-20.2,40.)); +#1657 = VECTOR('',#1658,1.); +#1658 = DIRECTION('',(-0.,1.,0.)); +#1659 = ORIENTED_EDGE('',*,*,#756,.T.); +#1660 = ORIENTED_EDGE('',*,*,#1637,.T.); +#1661 = ORIENTED_EDGE('',*,*,#1386,.F.); +#1662 = PLANE('',#1663); +#1663 = AXIS2_PLACEMENT_3D('',#1664,#1665,#1666); +#1664 = CARTESIAN_POINT('',(91.944444444444,-20.2,8.)); +#1665 = DIRECTION('',(1.,0.,-0.)); +#1666 = DIRECTION('',(0.,0.,1.)); +#1667 = ADVANCED_FACE('',(#1668),#1679,.T.); +#1668 = FACE_BOUND('',#1669,.T.); +#1669 = EDGE_LOOP('',(#1670,#1671,#1677,#1678)); +#1670 = ORIENTED_EDGE('',*,*,#1378,.F.); +#1671 = ORIENTED_EDGE('',*,*,#1672,.F.); +#1672 = EDGE_CURVE('',#741,#1371,#1673,.T.); +#1673 = LINE('',#1674,#1675); +#1674 = CARTESIAN_POINT('',(85.833333333333,-20.2,40.)); +#1675 = VECTOR('',#1676,1.); +#1676 = DIRECTION('',(-0.,1.,0.)); +#1677 = ORIENTED_EDGE('',*,*,#748,.T.); +#1678 = ORIENTED_EDGE('',*,*,#1654,.T.); +#1679 = PLANE('',#1680); +#1680 = AXIS2_PLACEMENT_3D('',#1681,#1682,#1683); +#1681 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#1682 = DIRECTION('',(0.,0.,1.)); +#1683 = DIRECTION('',(1.,0.,-0.)); +#1684 = ADVANCED_FACE('',(#1685),#1696,.F.); +#1685 = FACE_BOUND('',#1686,.F.); +#1686 = EDGE_LOOP('',(#1687,#1688,#1689,#1695)); +#1687 = ORIENTED_EDGE('',*,*,#1672,.F.); +#1688 = ORIENTED_EDGE('',*,*,#740,.T.); +#1689 = ORIENTED_EDGE('',*,*,#1690,.T.); +#1690 = EDGE_CURVE('',#733,#1363,#1691,.T.); +#1691 = LINE('',#1692,#1693); +#1692 = CARTESIAN_POINT('',(85.833333333333,-20.2,8.)); +#1693 = VECTOR('',#1694,1.); +#1694 = DIRECTION('',(-0.,1.,0.)); +#1695 = ORIENTED_EDGE('',*,*,#1370,.F.); +#1696 = PLANE('',#1697); +#1697 = AXIS2_PLACEMENT_3D('',#1698,#1699,#1700); +#1698 = CARTESIAN_POINT('',(85.833333333333,-20.2,8.)); +#1699 = DIRECTION('',(1.,0.,-0.)); +#1700 = DIRECTION('',(0.,0.,1.)); +#1701 = ADVANCED_FACE('',(#1702),#1713,.T.); +#1702 = FACE_BOUND('',#1703,.T.); +#1703 = EDGE_LOOP('',(#1704,#1710,#1711,#1712)); +#1704 = ORIENTED_EDGE('',*,*,#1705,.F.); +#1705 = EDGE_CURVE('',#725,#1355,#1706,.T.); +#1706 = LINE('',#1707,#1708); +#1707 = CARTESIAN_POINT('',(80.833333333333,-20.2,8.)); +#1708 = VECTOR('',#1709,1.); +#1709 = DIRECTION('',(-0.,1.,0.)); +#1710 = ORIENTED_EDGE('',*,*,#732,.T.); +#1711 = ORIENTED_EDGE('',*,*,#1690,.T.); +#1712 = ORIENTED_EDGE('',*,*,#1362,.F.); +#1713 = PLANE('',#1714); +#1714 = AXIS2_PLACEMENT_3D('',#1715,#1716,#1717); +#1715 = CARTESIAN_POINT('',(80.833333333333,-20.2,8.)); +#1716 = DIRECTION('',(0.,0.,1.)); +#1717 = DIRECTION('',(1.,0.,-0.)); +#1718 = ADVANCED_FACE('',(#1719),#1730,.T.); +#1719 = FACE_BOUND('',#1720,.T.); +#1720 = EDGE_LOOP('',(#1721,#1727,#1728,#1729)); +#1721 = ORIENTED_EDGE('',*,*,#1722,.F.); +#1722 = EDGE_CURVE('',#717,#1347,#1723,.T.); +#1723 = LINE('',#1724,#1725); +#1724 = CARTESIAN_POINT('',(80.833333333333,-20.2,40.)); +#1725 = VECTOR('',#1726,1.); +#1726 = DIRECTION('',(-0.,1.,0.)); +#1727 = ORIENTED_EDGE('',*,*,#724,.T.); +#1728 = ORIENTED_EDGE('',*,*,#1705,.T.); +#1729 = ORIENTED_EDGE('',*,*,#1354,.F.); +#1730 = PLANE('',#1731); +#1731 = AXIS2_PLACEMENT_3D('',#1732,#1733,#1734); +#1732 = CARTESIAN_POINT('',(80.833333333333,-20.2,8.)); +#1733 = DIRECTION('',(1.,0.,-0.)); +#1734 = DIRECTION('',(0.,0.,1.)); +#1735 = ADVANCED_FACE('',(#1736),#1747,.T.); +#1736 = FACE_BOUND('',#1737,.T.); +#1737 = EDGE_LOOP('',(#1738,#1739,#1745,#1746)); +#1738 = ORIENTED_EDGE('',*,*,#1346,.F.); +#1739 = ORIENTED_EDGE('',*,*,#1740,.F.); +#1740 = EDGE_CURVE('',#709,#1339,#1741,.T.); +#1741 = LINE('',#1742,#1743); +#1742 = CARTESIAN_POINT('',(74.722222222222,-20.2,40.)); +#1743 = VECTOR('',#1744,1.); +#1744 = DIRECTION('',(-0.,1.,0.)); +#1745 = ORIENTED_EDGE('',*,*,#716,.T.); +#1746 = ORIENTED_EDGE('',*,*,#1722,.T.); +#1747 = PLANE('',#1748); +#1748 = AXIS2_PLACEMENT_3D('',#1749,#1750,#1751); +#1749 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#1750 = DIRECTION('',(0.,0.,1.)); +#1751 = DIRECTION('',(1.,0.,-0.)); +#1752 = ADVANCED_FACE('',(#1753),#1764,.F.); +#1753 = FACE_BOUND('',#1754,.F.); +#1754 = EDGE_LOOP('',(#1755,#1756,#1757,#1763)); +#1755 = ORIENTED_EDGE('',*,*,#1740,.F.); +#1756 = ORIENTED_EDGE('',*,*,#708,.T.); +#1757 = ORIENTED_EDGE('',*,*,#1758,.T.); +#1758 = EDGE_CURVE('',#701,#1331,#1759,.T.); +#1759 = LINE('',#1760,#1761); +#1760 = CARTESIAN_POINT('',(74.722222222222,-20.2,8.)); +#1761 = VECTOR('',#1762,1.); +#1762 = DIRECTION('',(-0.,1.,0.)); +#1763 = ORIENTED_EDGE('',*,*,#1338,.F.); +#1764 = PLANE('',#1765); +#1765 = AXIS2_PLACEMENT_3D('',#1766,#1767,#1768); +#1766 = CARTESIAN_POINT('',(74.722222222222,-20.2,8.)); +#1767 = DIRECTION('',(1.,0.,-0.)); +#1768 = DIRECTION('',(0.,0.,1.)); +#1769 = ADVANCED_FACE('',(#1770),#1781,.T.); +#1770 = FACE_BOUND('',#1771,.T.); +#1771 = EDGE_LOOP('',(#1772,#1778,#1779,#1780)); +#1772 = ORIENTED_EDGE('',*,*,#1773,.F.); +#1773 = EDGE_CURVE('',#693,#1323,#1774,.T.); +#1774 = LINE('',#1775,#1776); +#1775 = CARTESIAN_POINT('',(69.722222222222,-20.2,8.)); +#1776 = VECTOR('',#1777,1.); +#1777 = DIRECTION('',(-0.,1.,0.)); +#1778 = ORIENTED_EDGE('',*,*,#700,.T.); +#1779 = ORIENTED_EDGE('',*,*,#1758,.T.); +#1780 = ORIENTED_EDGE('',*,*,#1330,.F.); +#1781 = PLANE('',#1782); +#1782 = AXIS2_PLACEMENT_3D('',#1783,#1784,#1785); +#1783 = CARTESIAN_POINT('',(69.722222222222,-20.2,8.)); +#1784 = DIRECTION('',(0.,0.,1.)); +#1785 = DIRECTION('',(1.,0.,-0.)); +#1786 = ADVANCED_FACE('',(#1787),#1798,.T.); +#1787 = FACE_BOUND('',#1788,.T.); +#1788 = EDGE_LOOP('',(#1789,#1795,#1796,#1797)); #1789 = ORIENTED_EDGE('',*,*,#1790,.F.); -#1790 = EDGE_CURVE('',#1767,#1783,#1791,.T.); +#1790 = EDGE_CURVE('',#685,#1315,#1791,.T.); #1791 = LINE('',#1792,#1793); -#1792 = CARTESIAN_POINT('',(-58.61111111111,-20.1,40.)); +#1792 = CARTESIAN_POINT('',(69.722222222222,-20.2,40.)); #1793 = VECTOR('',#1794,1.); #1794 = DIRECTION('',(-0.,1.,0.)); -#1795 = PLANE('',#1796); -#1796 = AXIS2_PLACEMENT_3D('',#1797,#1798,#1799); -#1797 = CARTESIAN_POINT('',(-58.61111111111,-20.1,8.)); -#1798 = DIRECTION('',(1.,0.,-0.)); -#1799 = DIRECTION('',(0.,0.,1.)); -#1800 = ADVANCED_FACE('',(#1801),#1817,.F.); -#1801 = FACE_BOUND('',#1802,.F.); -#1802 = EDGE_LOOP('',(#1803,#1809,#1810,#1816)); -#1803 = ORIENTED_EDGE('',*,*,#1804,.F.); -#1804 = EDGE_CURVE('',#1725,#1765,#1805,.T.); -#1805 = LINE('',#1806,#1807); -#1806 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1807 = VECTOR('',#1808,1.); -#1808 = DIRECTION('',(1.,0.,-0.)); -#1809 = ORIENTED_EDGE('',*,*,#1724,.T.); -#1810 = ORIENTED_EDGE('',*,*,#1811,.T.); -#1811 = EDGE_CURVE('',#1727,#1767,#1812,.T.); -#1812 = LINE('',#1813,#1814); -#1813 = CARTESIAN_POINT('',(-63.61111111111,-20.1,40.)); -#1814 = VECTOR('',#1815,1.); -#1815 = DIRECTION('',(1.,0.,-0.)); -#1816 = ORIENTED_EDGE('',*,*,#1764,.F.); -#1817 = PLANE('',#1818); -#1818 = AXIS2_PLACEMENT_3D('',#1819,#1820,#1821); -#1819 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1820 = DIRECTION('',(-0.,1.,0.)); -#1821 = DIRECTION('',(0.,0.,1.)); -#1822 = ADVANCED_FACE('',(#1823),#1839,.T.); -#1823 = FACE_BOUND('',#1824,.T.); -#1824 = EDGE_LOOP('',(#1825,#1831,#1832,#1838)); -#1825 = ORIENTED_EDGE('',*,*,#1826,.F.); -#1826 = EDGE_CURVE('',#1735,#1775,#1827,.T.); +#1795 = ORIENTED_EDGE('',*,*,#692,.T.); +#1796 = ORIENTED_EDGE('',*,*,#1773,.T.); +#1797 = ORIENTED_EDGE('',*,*,#1322,.F.); +#1798 = PLANE('',#1799); +#1799 = AXIS2_PLACEMENT_3D('',#1800,#1801,#1802); +#1800 = CARTESIAN_POINT('',(69.722222222222,-20.2,8.)); +#1801 = DIRECTION('',(1.,0.,-0.)); +#1802 = DIRECTION('',(0.,0.,1.)); +#1803 = ADVANCED_FACE('',(#1804),#1815,.T.); +#1804 = FACE_BOUND('',#1805,.T.); +#1805 = EDGE_LOOP('',(#1806,#1807,#1813,#1814)); +#1806 = ORIENTED_EDGE('',*,*,#1314,.F.); +#1807 = ORIENTED_EDGE('',*,*,#1808,.F.); +#1808 = EDGE_CURVE('',#677,#1307,#1809,.T.); +#1809 = LINE('',#1810,#1811); +#1810 = CARTESIAN_POINT('',(63.611111111111,-20.2,40.)); +#1811 = VECTOR('',#1812,1.); +#1812 = DIRECTION('',(-0.,1.,0.)); +#1813 = ORIENTED_EDGE('',*,*,#684,.T.); +#1814 = ORIENTED_EDGE('',*,*,#1790,.T.); +#1815 = PLANE('',#1816); +#1816 = AXIS2_PLACEMENT_3D('',#1817,#1818,#1819); +#1817 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#1818 = DIRECTION('',(0.,0.,1.)); +#1819 = DIRECTION('',(1.,0.,-0.)); +#1820 = ADVANCED_FACE('',(#1821),#1832,.F.); +#1821 = FACE_BOUND('',#1822,.F.); +#1822 = EDGE_LOOP('',(#1823,#1824,#1825,#1831)); +#1823 = ORIENTED_EDGE('',*,*,#1808,.F.); +#1824 = ORIENTED_EDGE('',*,*,#676,.T.); +#1825 = ORIENTED_EDGE('',*,*,#1826,.T.); +#1826 = EDGE_CURVE('',#669,#1299,#1827,.T.); #1827 = LINE('',#1828,#1829); -#1828 = CARTESIAN_POINT('',(-63.61111111111,-17.9,8.)); +#1828 = CARTESIAN_POINT('',(63.611111111111,-20.2,8.)); #1829 = VECTOR('',#1830,1.); -#1830 = DIRECTION('',(1.,0.,-0.)); -#1831 = ORIENTED_EDGE('',*,*,#1742,.T.); -#1832 = ORIENTED_EDGE('',*,*,#1833,.T.); -#1833 = EDGE_CURVE('',#1743,#1783,#1834,.T.); -#1834 = LINE('',#1835,#1836); -#1835 = CARTESIAN_POINT('',(-63.61111111111,-17.9,40.)); -#1836 = VECTOR('',#1837,1.); -#1837 = DIRECTION('',(1.,0.,-0.)); -#1838 = ORIENTED_EDGE('',*,*,#1782,.F.); -#1839 = PLANE('',#1840); -#1840 = AXIS2_PLACEMENT_3D('',#1841,#1842,#1843); -#1841 = CARTESIAN_POINT('',(-63.61111111111,-17.9,8.)); -#1842 = DIRECTION('',(-0.,1.,0.)); -#1843 = DIRECTION('',(0.,0.,1.)); -#1844 = ADVANCED_FACE('',(#1845),#1851,.F.); -#1845 = FACE_BOUND('',#1846,.F.); -#1846 = EDGE_LOOP('',(#1847,#1848,#1849,#1850)); -#1847 = ORIENTED_EDGE('',*,*,#1734,.F.); -#1848 = ORIENTED_EDGE('',*,*,#1804,.T.); -#1849 = ORIENTED_EDGE('',*,*,#1774,.T.); -#1850 = ORIENTED_EDGE('',*,*,#1826,.F.); -#1851 = PLANE('',#1852); -#1852 = AXIS2_PLACEMENT_3D('',#1853,#1854,#1855); -#1853 = CARTESIAN_POINT('',(-63.61111111111,-20.1,8.)); -#1854 = DIRECTION('',(0.,0.,1.)); -#1855 = DIRECTION('',(1.,0.,-0.)); -#1856 = ADVANCED_FACE('',(#1857),#1863,.T.); -#1857 = FACE_BOUND('',#1858,.T.); -#1858 = EDGE_LOOP('',(#1859,#1860,#1861,#1862)); -#1859 = ORIENTED_EDGE('',*,*,#1750,.F.); -#1860 = ORIENTED_EDGE('',*,*,#1811,.T.); -#1861 = ORIENTED_EDGE('',*,*,#1790,.T.); -#1862 = ORIENTED_EDGE('',*,*,#1833,.F.); -#1863 = PLANE('',#1864); -#1864 = AXIS2_PLACEMENT_3D('',#1865,#1866,#1867); -#1865 = CARTESIAN_POINT('',(-63.61111111111,-20.1,40.)); -#1866 = DIRECTION('',(0.,0.,1.)); -#1867 = DIRECTION('',(1.,0.,-0.)); -#1868 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1872)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#1869,#1870,#1871)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#1869 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#1870 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#1871 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#1872 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1869, - 'distance_accuracy_value','confusion accuracy'); -#1873 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1874,#1876); -#1874 = ( REPRESENTATION_RELATIONSHIP('','',#1717,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1875) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#1875 = ITEM_DEFINED_TRANSFORMATION('','',#11,#51); -#1876 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #1877); -#1877 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('10','WireDuct_LeftCombSlot_04', - '',#5,#1712,$); -#1878 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1714)); -#1879 = SHAPE_DEFINITION_REPRESENTATION(#1880,#1886); -#1880 = PRODUCT_DEFINITION_SHAPE('','',#1881); -#1881 = PRODUCT_DEFINITION('design','',#1882,#1885); -#1882 = PRODUCT_DEFINITION_FORMATION('','',#1883); -#1883 = PRODUCT('WireDuct_RightCombSlot_04','WireDuct_RightCombSlot_04', - '',(#1884)); -#1884 = PRODUCT_CONTEXT('',#2,'mechanical'); -#1885 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#1886 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1887),#2037); -#1887 = MANIFOLD_SOLID_BREP('',#1888); -#1888 = CLOSED_SHELL('',(#1889,#1929,#1969,#1991,#2013,#2025)); -#1889 = ADVANCED_FACE('',(#1890),#1924,.F.); -#1890 = FACE_BOUND('',#1891,.F.); -#1891 = EDGE_LOOP('',(#1892,#1902,#1910,#1918)); -#1892 = ORIENTED_EDGE('',*,*,#1893,.F.); -#1893 = EDGE_CURVE('',#1894,#1896,#1898,.T.); -#1894 = VERTEX_POINT('',#1895); -#1895 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#1896 = VERTEX_POINT('',#1897); -#1897 = CARTESIAN_POINT('',(-63.61111111111,17.9,40.)); -#1898 = LINE('',#1899,#1900); -#1899 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#1900 = VECTOR('',#1901,1.); -#1901 = DIRECTION('',(0.,0.,1.)); -#1902 = ORIENTED_EDGE('',*,*,#1903,.T.); -#1903 = EDGE_CURVE('',#1894,#1904,#1906,.T.); -#1904 = VERTEX_POINT('',#1905); -#1905 = CARTESIAN_POINT('',(-63.61111111111,20.1,8.)); -#1906 = LINE('',#1907,#1908); -#1907 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#1908 = VECTOR('',#1909,1.); -#1909 = DIRECTION('',(-0.,1.,0.)); -#1910 = ORIENTED_EDGE('',*,*,#1911,.T.); -#1911 = EDGE_CURVE('',#1904,#1912,#1914,.T.); -#1912 = VERTEX_POINT('',#1913); -#1913 = CARTESIAN_POINT('',(-63.61111111111,20.1,40.)); -#1914 = LINE('',#1915,#1916); -#1915 = CARTESIAN_POINT('',(-63.61111111111,20.1,8.)); -#1916 = VECTOR('',#1917,1.); -#1917 = DIRECTION('',(0.,0.,1.)); -#1918 = ORIENTED_EDGE('',*,*,#1919,.F.); -#1919 = EDGE_CURVE('',#1896,#1912,#1920,.T.); -#1920 = LINE('',#1921,#1922); -#1921 = CARTESIAN_POINT('',(-63.61111111111,17.9,40.)); -#1922 = VECTOR('',#1923,1.); -#1923 = DIRECTION('',(-0.,1.,0.)); -#1924 = PLANE('',#1925); -#1925 = AXIS2_PLACEMENT_3D('',#1926,#1927,#1928); -#1926 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#1927 = DIRECTION('',(1.,0.,-0.)); -#1928 = DIRECTION('',(0.,0.,1.)); -#1929 = ADVANCED_FACE('',(#1930),#1964,.T.); -#1930 = FACE_BOUND('',#1931,.T.); -#1931 = EDGE_LOOP('',(#1932,#1942,#1950,#1958)); -#1932 = ORIENTED_EDGE('',*,*,#1933,.F.); -#1933 = EDGE_CURVE('',#1934,#1936,#1938,.T.); -#1934 = VERTEX_POINT('',#1935); -#1935 = CARTESIAN_POINT('',(-58.61111111111,17.9,8.)); -#1936 = VERTEX_POINT('',#1937); -#1937 = CARTESIAN_POINT('',(-58.61111111111,17.9,40.)); -#1938 = LINE('',#1939,#1940); -#1939 = CARTESIAN_POINT('',(-58.61111111111,17.9,8.)); -#1940 = VECTOR('',#1941,1.); -#1941 = DIRECTION('',(0.,0.,1.)); -#1942 = ORIENTED_EDGE('',*,*,#1943,.T.); -#1943 = EDGE_CURVE('',#1934,#1944,#1946,.T.); -#1944 = VERTEX_POINT('',#1945); -#1945 = CARTESIAN_POINT('',(-58.61111111111,20.1,8.)); -#1946 = LINE('',#1947,#1948); -#1947 = CARTESIAN_POINT('',(-58.61111111111,17.9,8.)); -#1948 = VECTOR('',#1949,1.); -#1949 = DIRECTION('',(-0.,1.,0.)); -#1950 = ORIENTED_EDGE('',*,*,#1951,.T.); -#1951 = EDGE_CURVE('',#1944,#1952,#1954,.T.); -#1952 = VERTEX_POINT('',#1953); -#1953 = CARTESIAN_POINT('',(-58.61111111111,20.1,40.)); -#1954 = LINE('',#1955,#1956); -#1955 = CARTESIAN_POINT('',(-58.61111111111,20.1,8.)); -#1956 = VECTOR('',#1957,1.); -#1957 = DIRECTION('',(0.,0.,1.)); -#1958 = ORIENTED_EDGE('',*,*,#1959,.F.); -#1959 = EDGE_CURVE('',#1936,#1952,#1960,.T.); -#1960 = LINE('',#1961,#1962); -#1961 = CARTESIAN_POINT('',(-58.61111111111,17.9,40.)); -#1962 = VECTOR('',#1963,1.); -#1963 = DIRECTION('',(-0.,1.,0.)); -#1964 = PLANE('',#1965); -#1965 = AXIS2_PLACEMENT_3D('',#1966,#1967,#1968); -#1966 = CARTESIAN_POINT('',(-58.61111111111,17.9,8.)); -#1967 = DIRECTION('',(1.,0.,-0.)); -#1968 = DIRECTION('',(0.,0.,1.)); -#1969 = ADVANCED_FACE('',(#1970),#1986,.F.); -#1970 = FACE_BOUND('',#1971,.F.); -#1971 = EDGE_LOOP('',(#1972,#1978,#1979,#1985)); -#1972 = ORIENTED_EDGE('',*,*,#1973,.F.); -#1973 = EDGE_CURVE('',#1894,#1934,#1974,.T.); -#1974 = LINE('',#1975,#1976); -#1975 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#1976 = VECTOR('',#1977,1.); -#1977 = DIRECTION('',(1.,0.,-0.)); -#1978 = ORIENTED_EDGE('',*,*,#1893,.T.); -#1979 = ORIENTED_EDGE('',*,*,#1980,.T.); -#1980 = EDGE_CURVE('',#1896,#1936,#1981,.T.); -#1981 = LINE('',#1982,#1983); -#1982 = CARTESIAN_POINT('',(-63.61111111111,17.9,40.)); -#1983 = VECTOR('',#1984,1.); -#1984 = DIRECTION('',(1.,0.,-0.)); -#1985 = ORIENTED_EDGE('',*,*,#1933,.F.); -#1986 = PLANE('',#1987); -#1987 = AXIS2_PLACEMENT_3D('',#1988,#1989,#1990); -#1988 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#1989 = DIRECTION('',(-0.,1.,0.)); -#1990 = DIRECTION('',(0.,0.,1.)); -#1991 = ADVANCED_FACE('',(#1992),#2008,.T.); -#1992 = FACE_BOUND('',#1993,.T.); -#1993 = EDGE_LOOP('',(#1994,#2000,#2001,#2007)); -#1994 = ORIENTED_EDGE('',*,*,#1995,.F.); -#1995 = EDGE_CURVE('',#1904,#1944,#1996,.T.); -#1996 = LINE('',#1997,#1998); -#1997 = CARTESIAN_POINT('',(-63.61111111111,20.1,8.)); -#1998 = VECTOR('',#1999,1.); -#1999 = DIRECTION('',(1.,0.,-0.)); -#2000 = ORIENTED_EDGE('',*,*,#1911,.T.); -#2001 = ORIENTED_EDGE('',*,*,#2002,.T.); -#2002 = EDGE_CURVE('',#1912,#1952,#2003,.T.); -#2003 = LINE('',#2004,#2005); -#2004 = CARTESIAN_POINT('',(-63.61111111111,20.1,40.)); -#2005 = VECTOR('',#2006,1.); -#2006 = DIRECTION('',(1.,0.,-0.)); -#2007 = ORIENTED_EDGE('',*,*,#1951,.F.); -#2008 = PLANE('',#2009); -#2009 = AXIS2_PLACEMENT_3D('',#2010,#2011,#2012); -#2010 = CARTESIAN_POINT('',(-63.61111111111,20.1,8.)); -#2011 = DIRECTION('',(-0.,1.,0.)); -#2012 = DIRECTION('',(0.,0.,1.)); -#2013 = ADVANCED_FACE('',(#2014),#2020,.F.); -#2014 = FACE_BOUND('',#2015,.F.); -#2015 = EDGE_LOOP('',(#2016,#2017,#2018,#2019)); -#2016 = ORIENTED_EDGE('',*,*,#1903,.F.); -#2017 = ORIENTED_EDGE('',*,*,#1973,.T.); -#2018 = ORIENTED_EDGE('',*,*,#1943,.T.); -#2019 = ORIENTED_EDGE('',*,*,#1995,.F.); -#2020 = PLANE('',#2021); -#2021 = AXIS2_PLACEMENT_3D('',#2022,#2023,#2024); -#2022 = CARTESIAN_POINT('',(-63.61111111111,17.9,8.)); -#2023 = DIRECTION('',(0.,0.,1.)); -#2024 = DIRECTION('',(1.,0.,-0.)); -#2025 = ADVANCED_FACE('',(#2026),#2032,.T.); -#2026 = FACE_BOUND('',#2027,.T.); -#2027 = EDGE_LOOP('',(#2028,#2029,#2030,#2031)); -#2028 = ORIENTED_EDGE('',*,*,#1919,.F.); -#2029 = ORIENTED_EDGE('',*,*,#1980,.T.); -#2030 = ORIENTED_EDGE('',*,*,#1959,.T.); -#2031 = ORIENTED_EDGE('',*,*,#2002,.F.); -#2032 = PLANE('',#2033); -#2033 = AXIS2_PLACEMENT_3D('',#2034,#2035,#2036); -#2034 = CARTESIAN_POINT('',(-63.61111111111,17.9,40.)); -#2035 = DIRECTION('',(0.,0.,1.)); -#2036 = DIRECTION('',(1.,0.,-0.)); -#2037 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2041)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#2038,#2039,#2040)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#2038 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#2039 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#2040 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#2041 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2038, - 'distance_accuracy_value','confusion accuracy'); -#2042 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2043,#2045); -#2043 = ( REPRESENTATION_RELATIONSHIP('','',#1886,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2044) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#2044 = ITEM_DEFINED_TRANSFORMATION('','',#11,#55); -#2045 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #2046); -#2046 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('11','WireDuct_RightCombSlot_04', - '',#5,#1881,$); -#2047 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1883)); -#2048 = SHAPE_DEFINITION_REPRESENTATION(#2049,#2055); -#2049 = PRODUCT_DEFINITION_SHAPE('','',#2050); -#2050 = PRODUCT_DEFINITION('design','',#2051,#2054); -#2051 = PRODUCT_DEFINITION_FORMATION('','',#2052); -#2052 = PRODUCT('WireDuct_LeftCombSlot_05','WireDuct_LeftCombSlot_05','' - ,(#2053)); -#2053 = PRODUCT_CONTEXT('',#2,'mechanical'); -#2054 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#2055 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2056),#2206); -#2056 = MANIFOLD_SOLID_BREP('',#2057); -#2057 = CLOSED_SHELL('',(#2058,#2098,#2138,#2160,#2182,#2194)); -#2058 = ADVANCED_FACE('',(#2059),#2093,.F.); -#2059 = FACE_BOUND('',#2060,.F.); -#2060 = EDGE_LOOP('',(#2061,#2071,#2079,#2087)); +#1830 = DIRECTION('',(-0.,1.,0.)); +#1831 = ORIENTED_EDGE('',*,*,#1306,.F.); +#1832 = PLANE('',#1833); +#1833 = AXIS2_PLACEMENT_3D('',#1834,#1835,#1836); +#1834 = CARTESIAN_POINT('',(63.611111111111,-20.2,8.)); +#1835 = DIRECTION('',(1.,0.,-0.)); +#1836 = DIRECTION('',(0.,0.,1.)); +#1837 = ADVANCED_FACE('',(#1838),#1849,.T.); +#1838 = FACE_BOUND('',#1839,.T.); +#1839 = EDGE_LOOP('',(#1840,#1846,#1847,#1848)); +#1840 = ORIENTED_EDGE('',*,*,#1841,.F.); +#1841 = EDGE_CURVE('',#661,#1291,#1842,.T.); +#1842 = LINE('',#1843,#1844); +#1843 = CARTESIAN_POINT('',(58.611111111111,-20.2,8.)); +#1844 = VECTOR('',#1845,1.); +#1845 = DIRECTION('',(-0.,1.,0.)); +#1846 = ORIENTED_EDGE('',*,*,#668,.T.); +#1847 = ORIENTED_EDGE('',*,*,#1826,.T.); +#1848 = ORIENTED_EDGE('',*,*,#1298,.F.); +#1849 = PLANE('',#1850); +#1850 = AXIS2_PLACEMENT_3D('',#1851,#1852,#1853); +#1851 = CARTESIAN_POINT('',(58.611111111111,-20.2,8.)); +#1852 = DIRECTION('',(0.,0.,1.)); +#1853 = DIRECTION('',(1.,0.,-0.)); +#1854 = ADVANCED_FACE('',(#1855),#1866,.T.); +#1855 = FACE_BOUND('',#1856,.T.); +#1856 = EDGE_LOOP('',(#1857,#1863,#1864,#1865)); +#1857 = ORIENTED_EDGE('',*,*,#1858,.F.); +#1858 = EDGE_CURVE('',#653,#1283,#1859,.T.); +#1859 = LINE('',#1860,#1861); +#1860 = CARTESIAN_POINT('',(58.611111111111,-20.2,40.)); +#1861 = VECTOR('',#1862,1.); +#1862 = DIRECTION('',(-0.,1.,0.)); +#1863 = ORIENTED_EDGE('',*,*,#660,.T.); +#1864 = ORIENTED_EDGE('',*,*,#1841,.T.); +#1865 = ORIENTED_EDGE('',*,*,#1290,.F.); +#1866 = PLANE('',#1867); +#1867 = AXIS2_PLACEMENT_3D('',#1868,#1869,#1870); +#1868 = CARTESIAN_POINT('',(58.611111111111,-20.2,8.)); +#1869 = DIRECTION('',(1.,0.,-0.)); +#1870 = DIRECTION('',(0.,0.,1.)); +#1871 = ADVANCED_FACE('',(#1872),#1883,.T.); +#1872 = FACE_BOUND('',#1873,.T.); +#1873 = EDGE_LOOP('',(#1874,#1875,#1881,#1882)); +#1874 = ORIENTED_EDGE('',*,*,#1282,.F.); +#1875 = ORIENTED_EDGE('',*,*,#1876,.F.); +#1876 = EDGE_CURVE('',#645,#1275,#1877,.T.); +#1877 = LINE('',#1878,#1879); +#1878 = CARTESIAN_POINT('',(52.5,-20.2,40.)); +#1879 = VECTOR('',#1880,1.); +#1880 = DIRECTION('',(-0.,1.,0.)); +#1881 = ORIENTED_EDGE('',*,*,#652,.T.); +#1882 = ORIENTED_EDGE('',*,*,#1858,.T.); +#1883 = PLANE('',#1884); +#1884 = AXIS2_PLACEMENT_3D('',#1885,#1886,#1887); +#1885 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#1886 = DIRECTION('',(0.,0.,1.)); +#1887 = DIRECTION('',(1.,0.,-0.)); +#1888 = ADVANCED_FACE('',(#1889),#1900,.F.); +#1889 = FACE_BOUND('',#1890,.F.); +#1890 = EDGE_LOOP('',(#1891,#1892,#1893,#1899)); +#1891 = ORIENTED_EDGE('',*,*,#1876,.F.); +#1892 = ORIENTED_EDGE('',*,*,#644,.T.); +#1893 = ORIENTED_EDGE('',*,*,#1894,.T.); +#1894 = EDGE_CURVE('',#637,#1267,#1895,.T.); +#1895 = LINE('',#1896,#1897); +#1896 = CARTESIAN_POINT('',(52.5,-20.2,8.)); +#1897 = VECTOR('',#1898,1.); +#1898 = DIRECTION('',(-0.,1.,0.)); +#1899 = ORIENTED_EDGE('',*,*,#1274,.F.); +#1900 = PLANE('',#1901); +#1901 = AXIS2_PLACEMENT_3D('',#1902,#1903,#1904); +#1902 = CARTESIAN_POINT('',(52.5,-20.2,8.)); +#1903 = DIRECTION('',(1.,0.,-0.)); +#1904 = DIRECTION('',(0.,0.,1.)); +#1905 = ADVANCED_FACE('',(#1906),#1917,.T.); +#1906 = FACE_BOUND('',#1907,.T.); +#1907 = EDGE_LOOP('',(#1908,#1914,#1915,#1916)); +#1908 = ORIENTED_EDGE('',*,*,#1909,.F.); +#1909 = EDGE_CURVE('',#629,#1259,#1910,.T.); +#1910 = LINE('',#1911,#1912); +#1911 = CARTESIAN_POINT('',(47.5,-20.2,8.)); +#1912 = VECTOR('',#1913,1.); +#1913 = DIRECTION('',(-0.,1.,0.)); +#1914 = ORIENTED_EDGE('',*,*,#636,.T.); +#1915 = ORIENTED_EDGE('',*,*,#1894,.T.); +#1916 = ORIENTED_EDGE('',*,*,#1266,.F.); +#1917 = PLANE('',#1918); +#1918 = AXIS2_PLACEMENT_3D('',#1919,#1920,#1921); +#1919 = CARTESIAN_POINT('',(47.5,-20.2,8.)); +#1920 = DIRECTION('',(0.,0.,1.)); +#1921 = DIRECTION('',(1.,0.,-0.)); +#1922 = ADVANCED_FACE('',(#1923),#1934,.T.); +#1923 = FACE_BOUND('',#1924,.T.); +#1924 = EDGE_LOOP('',(#1925,#1931,#1932,#1933)); +#1925 = ORIENTED_EDGE('',*,*,#1926,.F.); +#1926 = EDGE_CURVE('',#621,#1251,#1927,.T.); +#1927 = LINE('',#1928,#1929); +#1928 = CARTESIAN_POINT('',(47.5,-20.2,40.)); +#1929 = VECTOR('',#1930,1.); +#1930 = DIRECTION('',(-0.,1.,0.)); +#1931 = ORIENTED_EDGE('',*,*,#628,.T.); +#1932 = ORIENTED_EDGE('',*,*,#1909,.T.); +#1933 = ORIENTED_EDGE('',*,*,#1258,.F.); +#1934 = PLANE('',#1935); +#1935 = AXIS2_PLACEMENT_3D('',#1936,#1937,#1938); +#1936 = CARTESIAN_POINT('',(47.5,-20.2,8.)); +#1937 = DIRECTION('',(1.,0.,-0.)); +#1938 = DIRECTION('',(0.,0.,1.)); +#1939 = ADVANCED_FACE('',(#1940),#1951,.T.); +#1940 = FACE_BOUND('',#1941,.T.); +#1941 = EDGE_LOOP('',(#1942,#1943,#1949,#1950)); +#1942 = ORIENTED_EDGE('',*,*,#1250,.F.); +#1943 = ORIENTED_EDGE('',*,*,#1944,.F.); +#1944 = EDGE_CURVE('',#613,#1243,#1945,.T.); +#1945 = LINE('',#1946,#1947); +#1946 = CARTESIAN_POINT('',(41.388888888889,-20.2,40.)); +#1947 = VECTOR('',#1948,1.); +#1948 = DIRECTION('',(-0.,1.,0.)); +#1949 = ORIENTED_EDGE('',*,*,#620,.T.); +#1950 = ORIENTED_EDGE('',*,*,#1926,.T.); +#1951 = PLANE('',#1952); +#1952 = AXIS2_PLACEMENT_3D('',#1953,#1954,#1955); +#1953 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#1954 = DIRECTION('',(0.,0.,1.)); +#1955 = DIRECTION('',(1.,0.,-0.)); +#1956 = ADVANCED_FACE('',(#1957),#1968,.F.); +#1957 = FACE_BOUND('',#1958,.F.); +#1958 = EDGE_LOOP('',(#1959,#1960,#1961,#1967)); +#1959 = ORIENTED_EDGE('',*,*,#1944,.F.); +#1960 = ORIENTED_EDGE('',*,*,#612,.T.); +#1961 = ORIENTED_EDGE('',*,*,#1962,.T.); +#1962 = EDGE_CURVE('',#605,#1235,#1963,.T.); +#1963 = LINE('',#1964,#1965); +#1964 = CARTESIAN_POINT('',(41.388888888889,-20.2,8.)); +#1965 = VECTOR('',#1966,1.); +#1966 = DIRECTION('',(-0.,1.,0.)); +#1967 = ORIENTED_EDGE('',*,*,#1242,.F.); +#1968 = PLANE('',#1969); +#1969 = AXIS2_PLACEMENT_3D('',#1970,#1971,#1972); +#1970 = CARTESIAN_POINT('',(41.388888888889,-20.2,8.)); +#1971 = DIRECTION('',(1.,0.,-0.)); +#1972 = DIRECTION('',(0.,0.,1.)); +#1973 = ADVANCED_FACE('',(#1974),#1985,.T.); +#1974 = FACE_BOUND('',#1975,.T.); +#1975 = EDGE_LOOP('',(#1976,#1982,#1983,#1984)); +#1976 = ORIENTED_EDGE('',*,*,#1977,.F.); +#1977 = EDGE_CURVE('',#597,#1227,#1978,.T.); +#1978 = LINE('',#1979,#1980); +#1979 = CARTESIAN_POINT('',(36.388888888889,-20.2,8.)); +#1980 = VECTOR('',#1981,1.); +#1981 = DIRECTION('',(-0.,1.,0.)); +#1982 = ORIENTED_EDGE('',*,*,#604,.T.); +#1983 = ORIENTED_EDGE('',*,*,#1962,.T.); +#1984 = ORIENTED_EDGE('',*,*,#1234,.F.); +#1985 = PLANE('',#1986); +#1986 = AXIS2_PLACEMENT_3D('',#1987,#1988,#1989); +#1987 = CARTESIAN_POINT('',(36.388888888889,-20.2,8.)); +#1988 = DIRECTION('',(0.,0.,1.)); +#1989 = DIRECTION('',(1.,0.,-0.)); +#1990 = ADVANCED_FACE('',(#1991),#2002,.T.); +#1991 = FACE_BOUND('',#1992,.T.); +#1992 = EDGE_LOOP('',(#1993,#1999,#2000,#2001)); +#1993 = ORIENTED_EDGE('',*,*,#1994,.F.); +#1994 = EDGE_CURVE('',#589,#1219,#1995,.T.); +#1995 = LINE('',#1996,#1997); +#1996 = CARTESIAN_POINT('',(36.388888888889,-20.2,40.)); +#1997 = VECTOR('',#1998,1.); +#1998 = DIRECTION('',(-0.,1.,0.)); +#1999 = ORIENTED_EDGE('',*,*,#596,.T.); +#2000 = ORIENTED_EDGE('',*,*,#1977,.T.); +#2001 = ORIENTED_EDGE('',*,*,#1226,.F.); +#2002 = PLANE('',#2003); +#2003 = AXIS2_PLACEMENT_3D('',#2004,#2005,#2006); +#2004 = CARTESIAN_POINT('',(36.388888888889,-20.2,8.)); +#2005 = DIRECTION('',(1.,0.,-0.)); +#2006 = DIRECTION('',(0.,0.,1.)); +#2007 = ADVANCED_FACE('',(#2008),#2019,.T.); +#2008 = FACE_BOUND('',#2009,.T.); +#2009 = EDGE_LOOP('',(#2010,#2011,#2017,#2018)); +#2010 = ORIENTED_EDGE('',*,*,#1218,.F.); +#2011 = ORIENTED_EDGE('',*,*,#2012,.F.); +#2012 = EDGE_CURVE('',#581,#1211,#2013,.T.); +#2013 = LINE('',#2014,#2015); +#2014 = CARTESIAN_POINT('',(30.277777777778,-20.2,40.)); +#2015 = VECTOR('',#2016,1.); +#2016 = DIRECTION('',(-0.,1.,0.)); +#2017 = ORIENTED_EDGE('',*,*,#588,.T.); +#2018 = ORIENTED_EDGE('',*,*,#1994,.T.); +#2019 = PLANE('',#2020); +#2020 = AXIS2_PLACEMENT_3D('',#2021,#2022,#2023); +#2021 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2022 = DIRECTION('',(0.,0.,1.)); +#2023 = DIRECTION('',(1.,0.,-0.)); +#2024 = ADVANCED_FACE('',(#2025),#2036,.F.); +#2025 = FACE_BOUND('',#2026,.F.); +#2026 = EDGE_LOOP('',(#2027,#2028,#2029,#2035)); +#2027 = ORIENTED_EDGE('',*,*,#2012,.F.); +#2028 = ORIENTED_EDGE('',*,*,#580,.T.); +#2029 = ORIENTED_EDGE('',*,*,#2030,.T.); +#2030 = EDGE_CURVE('',#573,#1203,#2031,.T.); +#2031 = LINE('',#2032,#2033); +#2032 = CARTESIAN_POINT('',(30.277777777778,-20.2,8.)); +#2033 = VECTOR('',#2034,1.); +#2034 = DIRECTION('',(-0.,1.,0.)); +#2035 = ORIENTED_EDGE('',*,*,#1210,.F.); +#2036 = PLANE('',#2037); +#2037 = AXIS2_PLACEMENT_3D('',#2038,#2039,#2040); +#2038 = CARTESIAN_POINT('',(30.277777777778,-20.2,8.)); +#2039 = DIRECTION('',(1.,0.,-0.)); +#2040 = DIRECTION('',(0.,0.,1.)); +#2041 = ADVANCED_FACE('',(#2042),#2053,.T.); +#2042 = FACE_BOUND('',#2043,.T.); +#2043 = EDGE_LOOP('',(#2044,#2050,#2051,#2052)); +#2044 = ORIENTED_EDGE('',*,*,#2045,.F.); +#2045 = EDGE_CURVE('',#565,#1195,#2046,.T.); +#2046 = LINE('',#2047,#2048); +#2047 = CARTESIAN_POINT('',(25.277777777778,-20.2,8.)); +#2048 = VECTOR('',#2049,1.); +#2049 = DIRECTION('',(-0.,1.,0.)); +#2050 = ORIENTED_EDGE('',*,*,#572,.T.); +#2051 = ORIENTED_EDGE('',*,*,#2030,.T.); +#2052 = ORIENTED_EDGE('',*,*,#1202,.F.); +#2053 = PLANE('',#2054); +#2054 = AXIS2_PLACEMENT_3D('',#2055,#2056,#2057); +#2055 = CARTESIAN_POINT('',(25.277777777778,-20.2,8.)); +#2056 = DIRECTION('',(0.,0.,1.)); +#2057 = DIRECTION('',(1.,0.,-0.)); +#2058 = ADVANCED_FACE('',(#2059),#2070,.T.); +#2059 = FACE_BOUND('',#2060,.T.); +#2060 = EDGE_LOOP('',(#2061,#2067,#2068,#2069)); #2061 = ORIENTED_EDGE('',*,*,#2062,.F.); -#2062 = EDGE_CURVE('',#2063,#2065,#2067,.T.); -#2063 = VERTEX_POINT('',#2064); -#2064 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); -#2065 = VERTEX_POINT('',#2066); -#2066 = CARTESIAN_POINT('',(-52.5,-20.1,40.)); -#2067 = LINE('',#2068,#2069); -#2068 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); -#2069 = VECTOR('',#2070,1.); -#2070 = DIRECTION('',(0.,0.,1.)); -#2071 = ORIENTED_EDGE('',*,*,#2072,.T.); -#2072 = EDGE_CURVE('',#2063,#2073,#2075,.T.); -#2073 = VERTEX_POINT('',#2074); -#2074 = CARTESIAN_POINT('',(-52.5,-17.9,8.)); -#2075 = LINE('',#2076,#2077); -#2076 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); -#2077 = VECTOR('',#2078,1.); -#2078 = DIRECTION('',(-0.,1.,0.)); -#2079 = ORIENTED_EDGE('',*,*,#2080,.T.); -#2080 = EDGE_CURVE('',#2073,#2081,#2083,.T.); -#2081 = VERTEX_POINT('',#2082); -#2082 = CARTESIAN_POINT('',(-52.5,-17.9,40.)); -#2083 = LINE('',#2084,#2085); -#2084 = CARTESIAN_POINT('',(-52.5,-17.9,8.)); -#2085 = VECTOR('',#2086,1.); -#2086 = DIRECTION('',(0.,0.,1.)); -#2087 = ORIENTED_EDGE('',*,*,#2088,.F.); -#2088 = EDGE_CURVE('',#2065,#2081,#2089,.T.); -#2089 = LINE('',#2090,#2091); -#2090 = CARTESIAN_POINT('',(-52.5,-20.1,40.)); -#2091 = VECTOR('',#2092,1.); -#2092 = DIRECTION('',(-0.,1.,0.)); -#2093 = PLANE('',#2094); -#2094 = AXIS2_PLACEMENT_3D('',#2095,#2096,#2097); -#2095 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); -#2096 = DIRECTION('',(1.,0.,-0.)); -#2097 = DIRECTION('',(0.,0.,1.)); -#2098 = ADVANCED_FACE('',(#2099),#2133,.T.); -#2099 = FACE_BOUND('',#2100,.T.); -#2100 = EDGE_LOOP('',(#2101,#2111,#2119,#2127)); -#2101 = ORIENTED_EDGE('',*,*,#2102,.F.); -#2102 = EDGE_CURVE('',#2103,#2105,#2107,.T.); -#2103 = VERTEX_POINT('',#2104); -#2104 = CARTESIAN_POINT('',(-47.5,-20.1,8.)); -#2105 = VERTEX_POINT('',#2106); -#2106 = CARTESIAN_POINT('',(-47.5,-20.1,40.)); -#2107 = LINE('',#2108,#2109); -#2108 = CARTESIAN_POINT('',(-47.5,-20.1,8.)); -#2109 = VECTOR('',#2110,1.); -#2110 = DIRECTION('',(0.,0.,1.)); -#2111 = ORIENTED_EDGE('',*,*,#2112,.T.); -#2112 = EDGE_CURVE('',#2103,#2113,#2115,.T.); -#2113 = VERTEX_POINT('',#2114); -#2114 = CARTESIAN_POINT('',(-47.5,-17.9,8.)); -#2115 = LINE('',#2116,#2117); -#2116 = CARTESIAN_POINT('',(-47.5,-20.1,8.)); -#2117 = VECTOR('',#2118,1.); -#2118 = DIRECTION('',(-0.,1.,0.)); -#2119 = ORIENTED_EDGE('',*,*,#2120,.T.); -#2120 = EDGE_CURVE('',#2113,#2121,#2123,.T.); -#2121 = VERTEX_POINT('',#2122); -#2122 = CARTESIAN_POINT('',(-47.5,-17.9,40.)); -#2123 = LINE('',#2124,#2125); -#2124 = CARTESIAN_POINT('',(-47.5,-17.9,8.)); -#2125 = VECTOR('',#2126,1.); -#2126 = DIRECTION('',(0.,0.,1.)); -#2127 = ORIENTED_EDGE('',*,*,#2128,.F.); -#2128 = EDGE_CURVE('',#2105,#2121,#2129,.T.); -#2129 = LINE('',#2130,#2131); -#2130 = CARTESIAN_POINT('',(-47.5,-20.1,40.)); -#2131 = VECTOR('',#2132,1.); -#2132 = DIRECTION('',(-0.,1.,0.)); -#2133 = PLANE('',#2134); -#2134 = AXIS2_PLACEMENT_3D('',#2135,#2136,#2137); -#2135 = CARTESIAN_POINT('',(-47.5,-20.1,8.)); -#2136 = DIRECTION('',(1.,0.,-0.)); -#2137 = DIRECTION('',(0.,0.,1.)); -#2138 = ADVANCED_FACE('',(#2139),#2155,.F.); -#2139 = FACE_BOUND('',#2140,.F.); -#2140 = EDGE_LOOP('',(#2141,#2147,#2148,#2154)); -#2141 = ORIENTED_EDGE('',*,*,#2142,.F.); -#2142 = EDGE_CURVE('',#2063,#2103,#2143,.T.); -#2143 = LINE('',#2144,#2145); -#2144 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); -#2145 = VECTOR('',#2146,1.); -#2146 = DIRECTION('',(1.,0.,-0.)); -#2147 = ORIENTED_EDGE('',*,*,#2062,.T.); -#2148 = ORIENTED_EDGE('',*,*,#2149,.T.); -#2149 = EDGE_CURVE('',#2065,#2105,#2150,.T.); -#2150 = LINE('',#2151,#2152); -#2151 = CARTESIAN_POINT('',(-52.5,-20.1,40.)); -#2152 = VECTOR('',#2153,1.); -#2153 = DIRECTION('',(1.,0.,-0.)); -#2154 = ORIENTED_EDGE('',*,*,#2102,.F.); +#2062 = EDGE_CURVE('',#557,#1187,#2063,.T.); +#2063 = LINE('',#2064,#2065); +#2064 = CARTESIAN_POINT('',(25.277777777778,-20.2,40.)); +#2065 = VECTOR('',#2066,1.); +#2066 = DIRECTION('',(-0.,1.,0.)); +#2067 = ORIENTED_EDGE('',*,*,#564,.T.); +#2068 = ORIENTED_EDGE('',*,*,#2045,.T.); +#2069 = ORIENTED_EDGE('',*,*,#1194,.F.); +#2070 = PLANE('',#2071); +#2071 = AXIS2_PLACEMENT_3D('',#2072,#2073,#2074); +#2072 = CARTESIAN_POINT('',(25.277777777778,-20.2,8.)); +#2073 = DIRECTION('',(1.,0.,-0.)); +#2074 = DIRECTION('',(0.,0.,1.)); +#2075 = ADVANCED_FACE('',(#2076),#2087,.T.); +#2076 = FACE_BOUND('',#2077,.T.); +#2077 = EDGE_LOOP('',(#2078,#2079,#2085,#2086)); +#2078 = ORIENTED_EDGE('',*,*,#1186,.F.); +#2079 = ORIENTED_EDGE('',*,*,#2080,.F.); +#2080 = EDGE_CURVE('',#549,#1179,#2081,.T.); +#2081 = LINE('',#2082,#2083); +#2082 = CARTESIAN_POINT('',(19.166666666667,-20.2,40.)); +#2083 = VECTOR('',#2084,1.); +#2084 = DIRECTION('',(-0.,1.,0.)); +#2085 = ORIENTED_EDGE('',*,*,#556,.T.); +#2086 = ORIENTED_EDGE('',*,*,#2062,.T.); +#2087 = PLANE('',#2088); +#2088 = AXIS2_PLACEMENT_3D('',#2089,#2090,#2091); +#2089 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2090 = DIRECTION('',(0.,0.,1.)); +#2091 = DIRECTION('',(1.,0.,-0.)); +#2092 = ADVANCED_FACE('',(#2093),#2104,.F.); +#2093 = FACE_BOUND('',#2094,.F.); +#2094 = EDGE_LOOP('',(#2095,#2096,#2097,#2103)); +#2095 = ORIENTED_EDGE('',*,*,#2080,.F.); +#2096 = ORIENTED_EDGE('',*,*,#548,.T.); +#2097 = ORIENTED_EDGE('',*,*,#2098,.T.); +#2098 = EDGE_CURVE('',#541,#1171,#2099,.T.); +#2099 = LINE('',#2100,#2101); +#2100 = CARTESIAN_POINT('',(19.166666666667,-20.2,8.)); +#2101 = VECTOR('',#2102,1.); +#2102 = DIRECTION('',(-0.,1.,0.)); +#2103 = ORIENTED_EDGE('',*,*,#1178,.F.); +#2104 = PLANE('',#2105); +#2105 = AXIS2_PLACEMENT_3D('',#2106,#2107,#2108); +#2106 = CARTESIAN_POINT('',(19.166666666667,-20.2,8.)); +#2107 = DIRECTION('',(1.,0.,-0.)); +#2108 = DIRECTION('',(0.,0.,1.)); +#2109 = ADVANCED_FACE('',(#2110),#2121,.T.); +#2110 = FACE_BOUND('',#2111,.T.); +#2111 = EDGE_LOOP('',(#2112,#2118,#2119,#2120)); +#2112 = ORIENTED_EDGE('',*,*,#2113,.F.); +#2113 = EDGE_CURVE('',#533,#1163,#2114,.T.); +#2114 = LINE('',#2115,#2116); +#2115 = CARTESIAN_POINT('',(14.166666666667,-20.2,8.)); +#2116 = VECTOR('',#2117,1.); +#2117 = DIRECTION('',(-0.,1.,0.)); +#2118 = ORIENTED_EDGE('',*,*,#540,.T.); +#2119 = ORIENTED_EDGE('',*,*,#2098,.T.); +#2120 = ORIENTED_EDGE('',*,*,#1170,.F.); +#2121 = PLANE('',#2122); +#2122 = AXIS2_PLACEMENT_3D('',#2123,#2124,#2125); +#2123 = CARTESIAN_POINT('',(14.166666666667,-20.2,8.)); +#2124 = DIRECTION('',(0.,0.,1.)); +#2125 = DIRECTION('',(1.,0.,-0.)); +#2126 = ADVANCED_FACE('',(#2127),#2138,.T.); +#2127 = FACE_BOUND('',#2128,.T.); +#2128 = EDGE_LOOP('',(#2129,#2135,#2136,#2137)); +#2129 = ORIENTED_EDGE('',*,*,#2130,.F.); +#2130 = EDGE_CURVE('',#525,#1155,#2131,.T.); +#2131 = LINE('',#2132,#2133); +#2132 = CARTESIAN_POINT('',(14.166666666667,-20.2,40.)); +#2133 = VECTOR('',#2134,1.); +#2134 = DIRECTION('',(-0.,1.,0.)); +#2135 = ORIENTED_EDGE('',*,*,#532,.T.); +#2136 = ORIENTED_EDGE('',*,*,#2113,.T.); +#2137 = ORIENTED_EDGE('',*,*,#1162,.F.); +#2138 = PLANE('',#2139); +#2139 = AXIS2_PLACEMENT_3D('',#2140,#2141,#2142); +#2140 = CARTESIAN_POINT('',(14.166666666667,-20.2,8.)); +#2141 = DIRECTION('',(1.,0.,-0.)); +#2142 = DIRECTION('',(0.,0.,1.)); +#2143 = ADVANCED_FACE('',(#2144),#2155,.T.); +#2144 = FACE_BOUND('',#2145,.T.); +#2145 = EDGE_LOOP('',(#2146,#2147,#2153,#2154)); +#2146 = ORIENTED_EDGE('',*,*,#1154,.F.); +#2147 = ORIENTED_EDGE('',*,*,#2148,.F.); +#2148 = EDGE_CURVE('',#517,#1147,#2149,.T.); +#2149 = LINE('',#2150,#2151); +#2150 = CARTESIAN_POINT('',(8.055555555556,-20.2,40.)); +#2151 = VECTOR('',#2152,1.); +#2152 = DIRECTION('',(-0.,1.,0.)); +#2153 = ORIENTED_EDGE('',*,*,#524,.T.); +#2154 = ORIENTED_EDGE('',*,*,#2130,.T.); #2155 = PLANE('',#2156); #2156 = AXIS2_PLACEMENT_3D('',#2157,#2158,#2159); -#2157 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); -#2158 = DIRECTION('',(-0.,1.,0.)); -#2159 = DIRECTION('',(0.,0.,1.)); -#2160 = ADVANCED_FACE('',(#2161),#2177,.T.); -#2161 = FACE_BOUND('',#2162,.T.); -#2162 = EDGE_LOOP('',(#2163,#2169,#2170,#2176)); -#2163 = ORIENTED_EDGE('',*,*,#2164,.F.); -#2164 = EDGE_CURVE('',#2073,#2113,#2165,.T.); -#2165 = LINE('',#2166,#2167); -#2166 = CARTESIAN_POINT('',(-52.5,-17.9,8.)); -#2167 = VECTOR('',#2168,1.); -#2168 = DIRECTION('',(1.,0.,-0.)); -#2169 = ORIENTED_EDGE('',*,*,#2080,.T.); -#2170 = ORIENTED_EDGE('',*,*,#2171,.T.); -#2171 = EDGE_CURVE('',#2081,#2121,#2172,.T.); -#2172 = LINE('',#2173,#2174); -#2173 = CARTESIAN_POINT('',(-52.5,-17.9,40.)); -#2174 = VECTOR('',#2175,1.); +#2157 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2158 = DIRECTION('',(0.,0.,1.)); +#2159 = DIRECTION('',(1.,0.,-0.)); +#2160 = ADVANCED_FACE('',(#2161),#2172,.F.); +#2161 = FACE_BOUND('',#2162,.F.); +#2162 = EDGE_LOOP('',(#2163,#2164,#2165,#2171)); +#2163 = ORIENTED_EDGE('',*,*,#2148,.F.); +#2164 = ORIENTED_EDGE('',*,*,#516,.T.); +#2165 = ORIENTED_EDGE('',*,*,#2166,.T.); +#2166 = EDGE_CURVE('',#509,#1139,#2167,.T.); +#2167 = LINE('',#2168,#2169); +#2168 = CARTESIAN_POINT('',(8.055555555556,-20.2,8.)); +#2169 = VECTOR('',#2170,1.); +#2170 = DIRECTION('',(-0.,1.,0.)); +#2171 = ORIENTED_EDGE('',*,*,#1146,.F.); +#2172 = PLANE('',#2173); +#2173 = AXIS2_PLACEMENT_3D('',#2174,#2175,#2176); +#2174 = CARTESIAN_POINT('',(8.055555555556,-20.2,8.)); #2175 = DIRECTION('',(1.,0.,-0.)); -#2176 = ORIENTED_EDGE('',*,*,#2120,.F.); -#2177 = PLANE('',#2178); -#2178 = AXIS2_PLACEMENT_3D('',#2179,#2180,#2181); -#2179 = CARTESIAN_POINT('',(-52.5,-17.9,8.)); -#2180 = DIRECTION('',(-0.,1.,0.)); -#2181 = DIRECTION('',(0.,0.,1.)); -#2182 = ADVANCED_FACE('',(#2183),#2189,.F.); -#2183 = FACE_BOUND('',#2184,.F.); -#2184 = EDGE_LOOP('',(#2185,#2186,#2187,#2188)); -#2185 = ORIENTED_EDGE('',*,*,#2072,.F.); -#2186 = ORIENTED_EDGE('',*,*,#2142,.T.); -#2187 = ORIENTED_EDGE('',*,*,#2112,.T.); -#2188 = ORIENTED_EDGE('',*,*,#2164,.F.); +#2176 = DIRECTION('',(0.,0.,1.)); +#2177 = ADVANCED_FACE('',(#2178),#2189,.T.); +#2178 = FACE_BOUND('',#2179,.T.); +#2179 = EDGE_LOOP('',(#2180,#2186,#2187,#2188)); +#2180 = ORIENTED_EDGE('',*,*,#2181,.F.); +#2181 = EDGE_CURVE('',#501,#1131,#2182,.T.); +#2182 = LINE('',#2183,#2184); +#2183 = CARTESIAN_POINT('',(3.055555555556,-20.2,8.)); +#2184 = VECTOR('',#2185,1.); +#2185 = DIRECTION('',(-0.,1.,0.)); +#2186 = ORIENTED_EDGE('',*,*,#508,.T.); +#2187 = ORIENTED_EDGE('',*,*,#2166,.T.); +#2188 = ORIENTED_EDGE('',*,*,#1138,.F.); #2189 = PLANE('',#2190); #2190 = AXIS2_PLACEMENT_3D('',#2191,#2192,#2193); -#2191 = CARTESIAN_POINT('',(-52.5,-20.1,8.)); +#2191 = CARTESIAN_POINT('',(3.055555555556,-20.2,8.)); #2192 = DIRECTION('',(0.,0.,1.)); #2193 = DIRECTION('',(1.,0.,-0.)); -#2194 = ADVANCED_FACE('',(#2195),#2201,.T.); +#2194 = ADVANCED_FACE('',(#2195),#2206,.T.); #2195 = FACE_BOUND('',#2196,.T.); -#2196 = EDGE_LOOP('',(#2197,#2198,#2199,#2200)); -#2197 = ORIENTED_EDGE('',*,*,#2088,.F.); -#2198 = ORIENTED_EDGE('',*,*,#2149,.T.); -#2199 = ORIENTED_EDGE('',*,*,#2128,.T.); -#2200 = ORIENTED_EDGE('',*,*,#2171,.F.); -#2201 = PLANE('',#2202); -#2202 = AXIS2_PLACEMENT_3D('',#2203,#2204,#2205); -#2203 = CARTESIAN_POINT('',(-52.5,-20.1,40.)); -#2204 = DIRECTION('',(0.,0.,1.)); -#2205 = DIRECTION('',(1.,0.,-0.)); -#2206 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2210)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#2207,#2208,#2209)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#2207 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#2208 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#2209 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#2210 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2207, - 'distance_accuracy_value','confusion accuracy'); -#2211 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2212,#2214); -#2212 = ( REPRESENTATION_RELATIONSHIP('','',#2055,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2213) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#2213 = ITEM_DEFINED_TRANSFORMATION('','',#11,#59); -#2214 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #2215); -#2215 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('12','WireDuct_LeftCombSlot_05', - '',#5,#2050,$); -#2216 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2052)); -#2217 = SHAPE_DEFINITION_REPRESENTATION(#2218,#2224); -#2218 = PRODUCT_DEFINITION_SHAPE('','',#2219); -#2219 = PRODUCT_DEFINITION('design','',#2220,#2223); -#2220 = PRODUCT_DEFINITION_FORMATION('','',#2221); -#2221 = PRODUCT('WireDuct_RightCombSlot_05','WireDuct_RightCombSlot_05', - '',(#2222)); -#2222 = PRODUCT_CONTEXT('',#2,'mechanical'); -#2223 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#2224 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2225),#2375); -#2225 = MANIFOLD_SOLID_BREP('',#2226); -#2226 = CLOSED_SHELL('',(#2227,#2267,#2307,#2329,#2351,#2363)); -#2227 = ADVANCED_FACE('',(#2228),#2262,.F.); -#2228 = FACE_BOUND('',#2229,.F.); -#2229 = EDGE_LOOP('',(#2230,#2240,#2248,#2256)); -#2230 = ORIENTED_EDGE('',*,*,#2231,.F.); -#2231 = EDGE_CURVE('',#2232,#2234,#2236,.T.); -#2232 = VERTEX_POINT('',#2233); -#2233 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2234 = VERTEX_POINT('',#2235); -#2235 = CARTESIAN_POINT('',(-52.5,17.9,40.)); -#2236 = LINE('',#2237,#2238); -#2237 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2238 = VECTOR('',#2239,1.); -#2239 = DIRECTION('',(0.,0.,1.)); -#2240 = ORIENTED_EDGE('',*,*,#2241,.T.); -#2241 = EDGE_CURVE('',#2232,#2242,#2244,.T.); -#2242 = VERTEX_POINT('',#2243); -#2243 = CARTESIAN_POINT('',(-52.5,20.1,8.)); -#2244 = LINE('',#2245,#2246); -#2245 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2246 = VECTOR('',#2247,1.); -#2247 = DIRECTION('',(-0.,1.,0.)); -#2248 = ORIENTED_EDGE('',*,*,#2249,.T.); -#2249 = EDGE_CURVE('',#2242,#2250,#2252,.T.); -#2250 = VERTEX_POINT('',#2251); -#2251 = CARTESIAN_POINT('',(-52.5,20.1,40.)); -#2252 = LINE('',#2253,#2254); -#2253 = CARTESIAN_POINT('',(-52.5,20.1,8.)); -#2254 = VECTOR('',#2255,1.); -#2255 = DIRECTION('',(0.,0.,1.)); -#2256 = ORIENTED_EDGE('',*,*,#2257,.F.); -#2257 = EDGE_CURVE('',#2234,#2250,#2258,.T.); -#2258 = LINE('',#2259,#2260); -#2259 = CARTESIAN_POINT('',(-52.5,17.9,40.)); -#2260 = VECTOR('',#2261,1.); -#2261 = DIRECTION('',(-0.,1.,0.)); -#2262 = PLANE('',#2263); -#2263 = AXIS2_PLACEMENT_3D('',#2264,#2265,#2266); -#2264 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2265 = DIRECTION('',(1.,0.,-0.)); -#2266 = DIRECTION('',(0.,0.,1.)); -#2267 = ADVANCED_FACE('',(#2268),#2302,.T.); -#2268 = FACE_BOUND('',#2269,.T.); -#2269 = EDGE_LOOP('',(#2270,#2280,#2288,#2296)); -#2270 = ORIENTED_EDGE('',*,*,#2271,.F.); -#2271 = EDGE_CURVE('',#2272,#2274,#2276,.T.); -#2272 = VERTEX_POINT('',#2273); -#2273 = CARTESIAN_POINT('',(-47.5,17.9,8.)); -#2274 = VERTEX_POINT('',#2275); -#2275 = CARTESIAN_POINT('',(-47.5,17.9,40.)); -#2276 = LINE('',#2277,#2278); -#2277 = CARTESIAN_POINT('',(-47.5,17.9,8.)); -#2278 = VECTOR('',#2279,1.); -#2279 = DIRECTION('',(0.,0.,1.)); -#2280 = ORIENTED_EDGE('',*,*,#2281,.T.); -#2281 = EDGE_CURVE('',#2272,#2282,#2284,.T.); -#2282 = VERTEX_POINT('',#2283); -#2283 = CARTESIAN_POINT('',(-47.5,20.1,8.)); -#2284 = LINE('',#2285,#2286); -#2285 = CARTESIAN_POINT('',(-47.5,17.9,8.)); -#2286 = VECTOR('',#2287,1.); -#2287 = DIRECTION('',(-0.,1.,0.)); -#2288 = ORIENTED_EDGE('',*,*,#2289,.T.); -#2289 = EDGE_CURVE('',#2282,#2290,#2292,.T.); -#2290 = VERTEX_POINT('',#2291); -#2291 = CARTESIAN_POINT('',(-47.5,20.1,40.)); -#2292 = LINE('',#2293,#2294); -#2293 = CARTESIAN_POINT('',(-47.5,20.1,8.)); -#2294 = VECTOR('',#2295,1.); -#2295 = DIRECTION('',(0.,0.,1.)); -#2296 = ORIENTED_EDGE('',*,*,#2297,.F.); -#2297 = EDGE_CURVE('',#2274,#2290,#2298,.T.); -#2298 = LINE('',#2299,#2300); -#2299 = CARTESIAN_POINT('',(-47.5,17.9,40.)); -#2300 = VECTOR('',#2301,1.); -#2301 = DIRECTION('',(-0.,1.,0.)); -#2302 = PLANE('',#2303); -#2303 = AXIS2_PLACEMENT_3D('',#2304,#2305,#2306); -#2304 = CARTESIAN_POINT('',(-47.5,17.9,8.)); -#2305 = DIRECTION('',(1.,0.,-0.)); -#2306 = DIRECTION('',(0.,0.,1.)); -#2307 = ADVANCED_FACE('',(#2308),#2324,.F.); -#2308 = FACE_BOUND('',#2309,.F.); -#2309 = EDGE_LOOP('',(#2310,#2316,#2317,#2323)); -#2310 = ORIENTED_EDGE('',*,*,#2311,.F.); -#2311 = EDGE_CURVE('',#2232,#2272,#2312,.T.); -#2312 = LINE('',#2313,#2314); -#2313 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2314 = VECTOR('',#2315,1.); -#2315 = DIRECTION('',(1.,0.,-0.)); -#2316 = ORIENTED_EDGE('',*,*,#2231,.T.); -#2317 = ORIENTED_EDGE('',*,*,#2318,.T.); -#2318 = EDGE_CURVE('',#2234,#2274,#2319,.T.); -#2319 = LINE('',#2320,#2321); -#2320 = CARTESIAN_POINT('',(-52.5,17.9,40.)); -#2321 = VECTOR('',#2322,1.); -#2322 = DIRECTION('',(1.,0.,-0.)); -#2323 = ORIENTED_EDGE('',*,*,#2271,.F.); -#2324 = PLANE('',#2325); -#2325 = AXIS2_PLACEMENT_3D('',#2326,#2327,#2328); -#2326 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2327 = DIRECTION('',(-0.,1.,0.)); +#2196 = EDGE_LOOP('',(#2197,#2203,#2204,#2205)); +#2197 = ORIENTED_EDGE('',*,*,#2198,.F.); +#2198 = EDGE_CURVE('',#493,#1123,#2199,.T.); +#2199 = LINE('',#2200,#2201); +#2200 = CARTESIAN_POINT('',(3.055555555556,-20.2,40.)); +#2201 = VECTOR('',#2202,1.); +#2202 = DIRECTION('',(-0.,1.,0.)); +#2203 = ORIENTED_EDGE('',*,*,#500,.T.); +#2204 = ORIENTED_EDGE('',*,*,#2181,.T.); +#2205 = ORIENTED_EDGE('',*,*,#1130,.F.); +#2206 = PLANE('',#2207); +#2207 = AXIS2_PLACEMENT_3D('',#2208,#2209,#2210); +#2208 = CARTESIAN_POINT('',(3.055555555556,-20.2,8.)); +#2209 = DIRECTION('',(1.,0.,-0.)); +#2210 = DIRECTION('',(0.,0.,1.)); +#2211 = ADVANCED_FACE('',(#2212),#2223,.T.); +#2212 = FACE_BOUND('',#2213,.T.); +#2213 = EDGE_LOOP('',(#2214,#2215,#2221,#2222)); +#2214 = ORIENTED_EDGE('',*,*,#1122,.F.); +#2215 = ORIENTED_EDGE('',*,*,#2216,.F.); +#2216 = EDGE_CURVE('',#485,#1115,#2217,.T.); +#2217 = LINE('',#2218,#2219); +#2218 = CARTESIAN_POINT('',(-3.055555555556,-20.2,40.)); +#2219 = VECTOR('',#2220,1.); +#2220 = DIRECTION('',(-0.,1.,0.)); +#2221 = ORIENTED_EDGE('',*,*,#492,.T.); +#2222 = ORIENTED_EDGE('',*,*,#2198,.T.); +#2223 = PLANE('',#2224); +#2224 = AXIS2_PLACEMENT_3D('',#2225,#2226,#2227); +#2225 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2226 = DIRECTION('',(0.,0.,1.)); +#2227 = DIRECTION('',(1.,0.,-0.)); +#2228 = ADVANCED_FACE('',(#2229),#2240,.F.); +#2229 = FACE_BOUND('',#2230,.F.); +#2230 = EDGE_LOOP('',(#2231,#2232,#2233,#2239)); +#2231 = ORIENTED_EDGE('',*,*,#2216,.F.); +#2232 = ORIENTED_EDGE('',*,*,#484,.T.); +#2233 = ORIENTED_EDGE('',*,*,#2234,.T.); +#2234 = EDGE_CURVE('',#477,#1107,#2235,.T.); +#2235 = LINE('',#2236,#2237); +#2236 = CARTESIAN_POINT('',(-3.055555555556,-20.2,8.)); +#2237 = VECTOR('',#2238,1.); +#2238 = DIRECTION('',(-0.,1.,0.)); +#2239 = ORIENTED_EDGE('',*,*,#1114,.F.); +#2240 = PLANE('',#2241); +#2241 = AXIS2_PLACEMENT_3D('',#2242,#2243,#2244); +#2242 = CARTESIAN_POINT('',(-3.055555555556,-20.2,8.)); +#2243 = DIRECTION('',(1.,0.,-0.)); +#2244 = DIRECTION('',(0.,0.,1.)); +#2245 = ADVANCED_FACE('',(#2246),#2257,.T.); +#2246 = FACE_BOUND('',#2247,.T.); +#2247 = EDGE_LOOP('',(#2248,#2254,#2255,#2256)); +#2248 = ORIENTED_EDGE('',*,*,#2249,.F.); +#2249 = EDGE_CURVE('',#469,#1099,#2250,.T.); +#2250 = LINE('',#2251,#2252); +#2251 = CARTESIAN_POINT('',(-8.055555555556,-20.2,8.)); +#2252 = VECTOR('',#2253,1.); +#2253 = DIRECTION('',(-0.,1.,0.)); +#2254 = ORIENTED_EDGE('',*,*,#476,.T.); +#2255 = ORIENTED_EDGE('',*,*,#2234,.T.); +#2256 = ORIENTED_EDGE('',*,*,#1106,.F.); +#2257 = PLANE('',#2258); +#2258 = AXIS2_PLACEMENT_3D('',#2259,#2260,#2261); +#2259 = CARTESIAN_POINT('',(-8.055555555556,-20.2,8.)); +#2260 = DIRECTION('',(0.,0.,1.)); +#2261 = DIRECTION('',(1.,0.,-0.)); +#2262 = ADVANCED_FACE('',(#2263),#2274,.T.); +#2263 = FACE_BOUND('',#2264,.T.); +#2264 = EDGE_LOOP('',(#2265,#2271,#2272,#2273)); +#2265 = ORIENTED_EDGE('',*,*,#2266,.F.); +#2266 = EDGE_CURVE('',#461,#1091,#2267,.T.); +#2267 = LINE('',#2268,#2269); +#2268 = CARTESIAN_POINT('',(-8.055555555556,-20.2,40.)); +#2269 = VECTOR('',#2270,1.); +#2270 = DIRECTION('',(-0.,1.,0.)); +#2271 = ORIENTED_EDGE('',*,*,#468,.T.); +#2272 = ORIENTED_EDGE('',*,*,#2249,.T.); +#2273 = ORIENTED_EDGE('',*,*,#1098,.F.); +#2274 = PLANE('',#2275); +#2275 = AXIS2_PLACEMENT_3D('',#2276,#2277,#2278); +#2276 = CARTESIAN_POINT('',(-8.055555555556,-20.2,8.)); +#2277 = DIRECTION('',(1.,0.,-0.)); +#2278 = DIRECTION('',(0.,0.,1.)); +#2279 = ADVANCED_FACE('',(#2280),#2291,.T.); +#2280 = FACE_BOUND('',#2281,.T.); +#2281 = EDGE_LOOP('',(#2282,#2283,#2289,#2290)); +#2282 = ORIENTED_EDGE('',*,*,#1090,.F.); +#2283 = ORIENTED_EDGE('',*,*,#2284,.F.); +#2284 = EDGE_CURVE('',#453,#1083,#2285,.T.); +#2285 = LINE('',#2286,#2287); +#2286 = CARTESIAN_POINT('',(-14.16666666666,-20.2,40.)); +#2287 = VECTOR('',#2288,1.); +#2288 = DIRECTION('',(-0.,1.,0.)); +#2289 = ORIENTED_EDGE('',*,*,#460,.T.); +#2290 = ORIENTED_EDGE('',*,*,#2266,.T.); +#2291 = PLANE('',#2292); +#2292 = AXIS2_PLACEMENT_3D('',#2293,#2294,#2295); +#2293 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2294 = DIRECTION('',(0.,0.,1.)); +#2295 = DIRECTION('',(1.,0.,-0.)); +#2296 = ADVANCED_FACE('',(#2297),#2308,.F.); +#2297 = FACE_BOUND('',#2298,.F.); +#2298 = EDGE_LOOP('',(#2299,#2300,#2301,#2307)); +#2299 = ORIENTED_EDGE('',*,*,#2284,.F.); +#2300 = ORIENTED_EDGE('',*,*,#452,.T.); +#2301 = ORIENTED_EDGE('',*,*,#2302,.T.); +#2302 = EDGE_CURVE('',#445,#1075,#2303,.T.); +#2303 = LINE('',#2304,#2305); +#2304 = CARTESIAN_POINT('',(-14.16666666666,-20.2,8.)); +#2305 = VECTOR('',#2306,1.); +#2306 = DIRECTION('',(-0.,1.,0.)); +#2307 = ORIENTED_EDGE('',*,*,#1082,.F.); +#2308 = PLANE('',#2309); +#2309 = AXIS2_PLACEMENT_3D('',#2310,#2311,#2312); +#2310 = CARTESIAN_POINT('',(-14.16666666666,-20.2,8.)); +#2311 = DIRECTION('',(1.,0.,-0.)); +#2312 = DIRECTION('',(0.,0.,1.)); +#2313 = ADVANCED_FACE('',(#2314),#2325,.T.); +#2314 = FACE_BOUND('',#2315,.T.); +#2315 = EDGE_LOOP('',(#2316,#2322,#2323,#2324)); +#2316 = ORIENTED_EDGE('',*,*,#2317,.F.); +#2317 = EDGE_CURVE('',#437,#1067,#2318,.T.); +#2318 = LINE('',#2319,#2320); +#2319 = CARTESIAN_POINT('',(-19.16666666666,-20.2,8.)); +#2320 = VECTOR('',#2321,1.); +#2321 = DIRECTION('',(-0.,1.,0.)); +#2322 = ORIENTED_EDGE('',*,*,#444,.T.); +#2323 = ORIENTED_EDGE('',*,*,#2302,.T.); +#2324 = ORIENTED_EDGE('',*,*,#1074,.F.); +#2325 = PLANE('',#2326); +#2326 = AXIS2_PLACEMENT_3D('',#2327,#2328,#2329); +#2327 = CARTESIAN_POINT('',(-19.16666666666,-20.2,8.)); #2328 = DIRECTION('',(0.,0.,1.)); -#2329 = ADVANCED_FACE('',(#2330),#2346,.T.); -#2330 = FACE_BOUND('',#2331,.T.); -#2331 = EDGE_LOOP('',(#2332,#2338,#2339,#2345)); -#2332 = ORIENTED_EDGE('',*,*,#2333,.F.); -#2333 = EDGE_CURVE('',#2242,#2282,#2334,.T.); -#2334 = LINE('',#2335,#2336); -#2335 = CARTESIAN_POINT('',(-52.5,20.1,8.)); -#2336 = VECTOR('',#2337,1.); -#2337 = DIRECTION('',(1.,0.,-0.)); -#2338 = ORIENTED_EDGE('',*,*,#2249,.T.); -#2339 = ORIENTED_EDGE('',*,*,#2340,.T.); -#2340 = EDGE_CURVE('',#2250,#2290,#2341,.T.); -#2341 = LINE('',#2342,#2343); -#2342 = CARTESIAN_POINT('',(-52.5,20.1,40.)); -#2343 = VECTOR('',#2344,1.); -#2344 = DIRECTION('',(1.,0.,-0.)); -#2345 = ORIENTED_EDGE('',*,*,#2289,.F.); -#2346 = PLANE('',#2347); -#2347 = AXIS2_PLACEMENT_3D('',#2348,#2349,#2350); -#2348 = CARTESIAN_POINT('',(-52.5,20.1,8.)); -#2349 = DIRECTION('',(-0.,1.,0.)); -#2350 = DIRECTION('',(0.,0.,1.)); -#2351 = ADVANCED_FACE('',(#2352),#2358,.F.); -#2352 = FACE_BOUND('',#2353,.F.); -#2353 = EDGE_LOOP('',(#2354,#2355,#2356,#2357)); -#2354 = ORIENTED_EDGE('',*,*,#2241,.F.); -#2355 = ORIENTED_EDGE('',*,*,#2311,.T.); -#2356 = ORIENTED_EDGE('',*,*,#2281,.T.); -#2357 = ORIENTED_EDGE('',*,*,#2333,.F.); -#2358 = PLANE('',#2359); -#2359 = AXIS2_PLACEMENT_3D('',#2360,#2361,#2362); -#2360 = CARTESIAN_POINT('',(-52.5,17.9,8.)); -#2361 = DIRECTION('',(0.,0.,1.)); -#2362 = DIRECTION('',(1.,0.,-0.)); -#2363 = ADVANCED_FACE('',(#2364),#2370,.T.); -#2364 = FACE_BOUND('',#2365,.T.); -#2365 = EDGE_LOOP('',(#2366,#2367,#2368,#2369)); -#2366 = ORIENTED_EDGE('',*,*,#2257,.F.); -#2367 = ORIENTED_EDGE('',*,*,#2318,.T.); -#2368 = ORIENTED_EDGE('',*,*,#2297,.T.); -#2369 = ORIENTED_EDGE('',*,*,#2340,.F.); -#2370 = PLANE('',#2371); -#2371 = AXIS2_PLACEMENT_3D('',#2372,#2373,#2374); -#2372 = CARTESIAN_POINT('',(-52.5,17.9,40.)); -#2373 = DIRECTION('',(0.,0.,1.)); -#2374 = DIRECTION('',(1.,0.,-0.)); -#2375 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2379)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#2376,#2377,#2378)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#2376 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#2377 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#2378 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#2379 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2376, - 'distance_accuracy_value','confusion accuracy'); -#2380 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2381,#2383); -#2381 = ( REPRESENTATION_RELATIONSHIP('','',#2224,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2382) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#2382 = ITEM_DEFINED_TRANSFORMATION('','',#11,#63); -#2383 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #2384); -#2384 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('13','WireDuct_RightCombSlot_05', - '',#5,#2219,$); -#2385 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2221)); -#2386 = SHAPE_DEFINITION_REPRESENTATION(#2387,#2393); -#2387 = PRODUCT_DEFINITION_SHAPE('','',#2388); -#2388 = PRODUCT_DEFINITION('design','',#2389,#2392); -#2389 = PRODUCT_DEFINITION_FORMATION('','',#2390); -#2390 = PRODUCT('WireDuct_LeftCombSlot_06','WireDuct_LeftCombSlot_06','' - ,(#2391)); -#2391 = PRODUCT_CONTEXT('',#2,'mechanical'); -#2392 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#2393 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2394),#2544); -#2394 = MANIFOLD_SOLID_BREP('',#2395); -#2395 = CLOSED_SHELL('',(#2396,#2436,#2476,#2498,#2520,#2532)); -#2396 = ADVANCED_FACE('',(#2397),#2431,.F.); -#2397 = FACE_BOUND('',#2398,.F.); -#2398 = EDGE_LOOP('',(#2399,#2409,#2417,#2425)); -#2399 = ORIENTED_EDGE('',*,*,#2400,.F.); -#2400 = EDGE_CURVE('',#2401,#2403,#2405,.T.); -#2401 = VERTEX_POINT('',#2402); -#2402 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2403 = VERTEX_POINT('',#2404); -#2404 = CARTESIAN_POINT('',(-41.38888888888,-20.1,40.)); -#2405 = LINE('',#2406,#2407); -#2406 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2407 = VECTOR('',#2408,1.); -#2408 = DIRECTION('',(0.,0.,1.)); -#2409 = ORIENTED_EDGE('',*,*,#2410,.T.); -#2410 = EDGE_CURVE('',#2401,#2411,#2413,.T.); -#2411 = VERTEX_POINT('',#2412); -#2412 = CARTESIAN_POINT('',(-41.38888888888,-17.9,8.)); -#2413 = LINE('',#2414,#2415); -#2414 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2415 = VECTOR('',#2416,1.); -#2416 = DIRECTION('',(-0.,1.,0.)); -#2417 = ORIENTED_EDGE('',*,*,#2418,.T.); -#2418 = EDGE_CURVE('',#2411,#2419,#2421,.T.); -#2419 = VERTEX_POINT('',#2420); -#2420 = CARTESIAN_POINT('',(-41.38888888888,-17.9,40.)); +#2329 = DIRECTION('',(1.,0.,-0.)); +#2330 = ADVANCED_FACE('',(#2331),#2342,.T.); +#2331 = FACE_BOUND('',#2332,.T.); +#2332 = EDGE_LOOP('',(#2333,#2339,#2340,#2341)); +#2333 = ORIENTED_EDGE('',*,*,#2334,.F.); +#2334 = EDGE_CURVE('',#429,#1059,#2335,.T.); +#2335 = LINE('',#2336,#2337); +#2336 = CARTESIAN_POINT('',(-19.16666666666,-20.2,40.)); +#2337 = VECTOR('',#2338,1.); +#2338 = DIRECTION('',(-0.,1.,0.)); +#2339 = ORIENTED_EDGE('',*,*,#436,.T.); +#2340 = ORIENTED_EDGE('',*,*,#2317,.T.); +#2341 = ORIENTED_EDGE('',*,*,#1066,.F.); +#2342 = PLANE('',#2343); +#2343 = AXIS2_PLACEMENT_3D('',#2344,#2345,#2346); +#2344 = CARTESIAN_POINT('',(-19.16666666666,-20.2,8.)); +#2345 = DIRECTION('',(1.,0.,-0.)); +#2346 = DIRECTION('',(0.,0.,1.)); +#2347 = ADVANCED_FACE('',(#2348),#2359,.T.); +#2348 = FACE_BOUND('',#2349,.T.); +#2349 = EDGE_LOOP('',(#2350,#2351,#2357,#2358)); +#2350 = ORIENTED_EDGE('',*,*,#1058,.F.); +#2351 = ORIENTED_EDGE('',*,*,#2352,.F.); +#2352 = EDGE_CURVE('',#421,#1051,#2353,.T.); +#2353 = LINE('',#2354,#2355); +#2354 = CARTESIAN_POINT('',(-25.27777777777,-20.2,40.)); +#2355 = VECTOR('',#2356,1.); +#2356 = DIRECTION('',(-0.,1.,0.)); +#2357 = ORIENTED_EDGE('',*,*,#428,.T.); +#2358 = ORIENTED_EDGE('',*,*,#2334,.T.); +#2359 = PLANE('',#2360); +#2360 = AXIS2_PLACEMENT_3D('',#2361,#2362,#2363); +#2361 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2362 = DIRECTION('',(0.,0.,1.)); +#2363 = DIRECTION('',(1.,0.,-0.)); +#2364 = ADVANCED_FACE('',(#2365),#2376,.F.); +#2365 = FACE_BOUND('',#2366,.F.); +#2366 = EDGE_LOOP('',(#2367,#2368,#2369,#2375)); +#2367 = ORIENTED_EDGE('',*,*,#2352,.F.); +#2368 = ORIENTED_EDGE('',*,*,#420,.T.); +#2369 = ORIENTED_EDGE('',*,*,#2370,.T.); +#2370 = EDGE_CURVE('',#413,#1043,#2371,.T.); +#2371 = LINE('',#2372,#2373); +#2372 = CARTESIAN_POINT('',(-25.27777777777,-20.2,8.)); +#2373 = VECTOR('',#2374,1.); +#2374 = DIRECTION('',(-0.,1.,0.)); +#2375 = ORIENTED_EDGE('',*,*,#1050,.F.); +#2376 = PLANE('',#2377); +#2377 = AXIS2_PLACEMENT_3D('',#2378,#2379,#2380); +#2378 = CARTESIAN_POINT('',(-25.27777777777,-20.2,8.)); +#2379 = DIRECTION('',(1.,0.,-0.)); +#2380 = DIRECTION('',(0.,0.,1.)); +#2381 = ADVANCED_FACE('',(#2382),#2393,.T.); +#2382 = FACE_BOUND('',#2383,.T.); +#2383 = EDGE_LOOP('',(#2384,#2390,#2391,#2392)); +#2384 = ORIENTED_EDGE('',*,*,#2385,.F.); +#2385 = EDGE_CURVE('',#405,#1035,#2386,.T.); +#2386 = LINE('',#2387,#2388); +#2387 = CARTESIAN_POINT('',(-30.27777777777,-20.2,8.)); +#2388 = VECTOR('',#2389,1.); +#2389 = DIRECTION('',(-0.,1.,0.)); +#2390 = ORIENTED_EDGE('',*,*,#412,.T.); +#2391 = ORIENTED_EDGE('',*,*,#2370,.T.); +#2392 = ORIENTED_EDGE('',*,*,#1042,.F.); +#2393 = PLANE('',#2394); +#2394 = AXIS2_PLACEMENT_3D('',#2395,#2396,#2397); +#2395 = CARTESIAN_POINT('',(-30.27777777777,-20.2,8.)); +#2396 = DIRECTION('',(0.,0.,1.)); +#2397 = DIRECTION('',(1.,0.,-0.)); +#2398 = ADVANCED_FACE('',(#2399),#2410,.T.); +#2399 = FACE_BOUND('',#2400,.T.); +#2400 = EDGE_LOOP('',(#2401,#2407,#2408,#2409)); +#2401 = ORIENTED_EDGE('',*,*,#2402,.F.); +#2402 = EDGE_CURVE('',#397,#1027,#2403,.T.); +#2403 = LINE('',#2404,#2405); +#2404 = CARTESIAN_POINT('',(-30.27777777777,-20.2,40.)); +#2405 = VECTOR('',#2406,1.); +#2406 = DIRECTION('',(-0.,1.,0.)); +#2407 = ORIENTED_EDGE('',*,*,#404,.T.); +#2408 = ORIENTED_EDGE('',*,*,#2385,.T.); +#2409 = ORIENTED_EDGE('',*,*,#1034,.F.); +#2410 = PLANE('',#2411); +#2411 = AXIS2_PLACEMENT_3D('',#2412,#2413,#2414); +#2412 = CARTESIAN_POINT('',(-30.27777777777,-20.2,8.)); +#2413 = DIRECTION('',(1.,0.,-0.)); +#2414 = DIRECTION('',(0.,0.,1.)); +#2415 = ADVANCED_FACE('',(#2416),#2427,.T.); +#2416 = FACE_BOUND('',#2417,.T.); +#2417 = EDGE_LOOP('',(#2418,#2419,#2425,#2426)); +#2418 = ORIENTED_EDGE('',*,*,#1026,.F.); +#2419 = ORIENTED_EDGE('',*,*,#2420,.F.); +#2420 = EDGE_CURVE('',#389,#1019,#2421,.T.); #2421 = LINE('',#2422,#2423); -#2422 = CARTESIAN_POINT('',(-41.38888888888,-17.9,8.)); +#2422 = CARTESIAN_POINT('',(-36.38888888888,-20.2,40.)); #2423 = VECTOR('',#2424,1.); -#2424 = DIRECTION('',(0.,0.,1.)); -#2425 = ORIENTED_EDGE('',*,*,#2426,.F.); -#2426 = EDGE_CURVE('',#2403,#2419,#2427,.T.); -#2427 = LINE('',#2428,#2429); -#2428 = CARTESIAN_POINT('',(-41.38888888888,-20.1,40.)); -#2429 = VECTOR('',#2430,1.); -#2430 = DIRECTION('',(-0.,1.,0.)); -#2431 = PLANE('',#2432); -#2432 = AXIS2_PLACEMENT_3D('',#2433,#2434,#2435); -#2433 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2434 = DIRECTION('',(1.,0.,-0.)); -#2435 = DIRECTION('',(0.,0.,1.)); -#2436 = ADVANCED_FACE('',(#2437),#2471,.T.); -#2437 = FACE_BOUND('',#2438,.T.); -#2438 = EDGE_LOOP('',(#2439,#2449,#2457,#2465)); -#2439 = ORIENTED_EDGE('',*,*,#2440,.F.); -#2440 = EDGE_CURVE('',#2441,#2443,#2445,.T.); -#2441 = VERTEX_POINT('',#2442); -#2442 = CARTESIAN_POINT('',(-36.38888888888,-20.1,8.)); -#2443 = VERTEX_POINT('',#2444); -#2444 = CARTESIAN_POINT('',(-36.38888888888,-20.1,40.)); -#2445 = LINE('',#2446,#2447); -#2446 = CARTESIAN_POINT('',(-36.38888888888,-20.1,8.)); -#2447 = VECTOR('',#2448,1.); +#2424 = DIRECTION('',(-0.,1.,0.)); +#2425 = ORIENTED_EDGE('',*,*,#396,.T.); +#2426 = ORIENTED_EDGE('',*,*,#2402,.T.); +#2427 = PLANE('',#2428); +#2428 = AXIS2_PLACEMENT_3D('',#2429,#2430,#2431); +#2429 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2430 = DIRECTION('',(0.,0.,1.)); +#2431 = DIRECTION('',(1.,0.,-0.)); +#2432 = ADVANCED_FACE('',(#2433),#2444,.F.); +#2433 = FACE_BOUND('',#2434,.F.); +#2434 = EDGE_LOOP('',(#2435,#2436,#2437,#2443)); +#2435 = ORIENTED_EDGE('',*,*,#2420,.F.); +#2436 = ORIENTED_EDGE('',*,*,#388,.T.); +#2437 = ORIENTED_EDGE('',*,*,#2438,.T.); +#2438 = EDGE_CURVE('',#381,#1011,#2439,.T.); +#2439 = LINE('',#2440,#2441); +#2440 = CARTESIAN_POINT('',(-36.38888888888,-20.2,8.)); +#2441 = VECTOR('',#2442,1.); +#2442 = DIRECTION('',(-0.,1.,0.)); +#2443 = ORIENTED_EDGE('',*,*,#1018,.F.); +#2444 = PLANE('',#2445); +#2445 = AXIS2_PLACEMENT_3D('',#2446,#2447,#2448); +#2446 = CARTESIAN_POINT('',(-36.38888888888,-20.2,8.)); +#2447 = DIRECTION('',(1.,0.,-0.)); #2448 = DIRECTION('',(0.,0.,1.)); -#2449 = ORIENTED_EDGE('',*,*,#2450,.T.); -#2450 = EDGE_CURVE('',#2441,#2451,#2453,.T.); -#2451 = VERTEX_POINT('',#2452); -#2452 = CARTESIAN_POINT('',(-36.38888888888,-17.9,8.)); -#2453 = LINE('',#2454,#2455); -#2454 = CARTESIAN_POINT('',(-36.38888888888,-20.1,8.)); -#2455 = VECTOR('',#2456,1.); -#2456 = DIRECTION('',(-0.,1.,0.)); -#2457 = ORIENTED_EDGE('',*,*,#2458,.T.); -#2458 = EDGE_CURVE('',#2451,#2459,#2461,.T.); -#2459 = VERTEX_POINT('',#2460); -#2460 = CARTESIAN_POINT('',(-36.38888888888,-17.9,40.)); -#2461 = LINE('',#2462,#2463); -#2462 = CARTESIAN_POINT('',(-36.38888888888,-17.9,8.)); -#2463 = VECTOR('',#2464,1.); +#2449 = ADVANCED_FACE('',(#2450),#2461,.T.); +#2450 = FACE_BOUND('',#2451,.T.); +#2451 = EDGE_LOOP('',(#2452,#2458,#2459,#2460)); +#2452 = ORIENTED_EDGE('',*,*,#2453,.F.); +#2453 = EDGE_CURVE('',#373,#1003,#2454,.T.); +#2454 = LINE('',#2455,#2456); +#2455 = CARTESIAN_POINT('',(-41.38888888888,-20.2,8.)); +#2456 = VECTOR('',#2457,1.); +#2457 = DIRECTION('',(-0.,1.,0.)); +#2458 = ORIENTED_EDGE('',*,*,#380,.T.); +#2459 = ORIENTED_EDGE('',*,*,#2438,.T.); +#2460 = ORIENTED_EDGE('',*,*,#1010,.F.); +#2461 = PLANE('',#2462); +#2462 = AXIS2_PLACEMENT_3D('',#2463,#2464,#2465); +#2463 = CARTESIAN_POINT('',(-41.38888888888,-20.2,8.)); #2464 = DIRECTION('',(0.,0.,1.)); -#2465 = ORIENTED_EDGE('',*,*,#2466,.F.); -#2466 = EDGE_CURVE('',#2443,#2459,#2467,.T.); -#2467 = LINE('',#2468,#2469); -#2468 = CARTESIAN_POINT('',(-36.38888888888,-20.1,40.)); -#2469 = VECTOR('',#2470,1.); -#2470 = DIRECTION('',(-0.,1.,0.)); -#2471 = PLANE('',#2472); -#2472 = AXIS2_PLACEMENT_3D('',#2473,#2474,#2475); -#2473 = CARTESIAN_POINT('',(-36.38888888888,-20.1,8.)); -#2474 = DIRECTION('',(1.,0.,-0.)); -#2475 = DIRECTION('',(0.,0.,1.)); -#2476 = ADVANCED_FACE('',(#2477),#2493,.F.); -#2477 = FACE_BOUND('',#2478,.F.); -#2478 = EDGE_LOOP('',(#2479,#2485,#2486,#2492)); -#2479 = ORIENTED_EDGE('',*,*,#2480,.F.); -#2480 = EDGE_CURVE('',#2401,#2441,#2481,.T.); -#2481 = LINE('',#2482,#2483); -#2482 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2483 = VECTOR('',#2484,1.); -#2484 = DIRECTION('',(1.,0.,-0.)); -#2485 = ORIENTED_EDGE('',*,*,#2400,.T.); -#2486 = ORIENTED_EDGE('',*,*,#2487,.T.); -#2487 = EDGE_CURVE('',#2403,#2443,#2488,.T.); -#2488 = LINE('',#2489,#2490); -#2489 = CARTESIAN_POINT('',(-41.38888888888,-20.1,40.)); -#2490 = VECTOR('',#2491,1.); -#2491 = DIRECTION('',(1.,0.,-0.)); -#2492 = ORIENTED_EDGE('',*,*,#2440,.F.); -#2493 = PLANE('',#2494); -#2494 = AXIS2_PLACEMENT_3D('',#2495,#2496,#2497); -#2495 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2496 = DIRECTION('',(-0.,1.,0.)); -#2497 = DIRECTION('',(0.,0.,1.)); -#2498 = ADVANCED_FACE('',(#2499),#2515,.T.); -#2499 = FACE_BOUND('',#2500,.T.); -#2500 = EDGE_LOOP('',(#2501,#2507,#2508,#2514)); -#2501 = ORIENTED_EDGE('',*,*,#2502,.F.); -#2502 = EDGE_CURVE('',#2411,#2451,#2503,.T.); -#2503 = LINE('',#2504,#2505); -#2504 = CARTESIAN_POINT('',(-41.38888888888,-17.9,8.)); -#2505 = VECTOR('',#2506,1.); -#2506 = DIRECTION('',(1.,0.,-0.)); -#2507 = ORIENTED_EDGE('',*,*,#2418,.T.); -#2508 = ORIENTED_EDGE('',*,*,#2509,.T.); -#2509 = EDGE_CURVE('',#2419,#2459,#2510,.T.); -#2510 = LINE('',#2511,#2512); -#2511 = CARTESIAN_POINT('',(-41.38888888888,-17.9,40.)); -#2512 = VECTOR('',#2513,1.); -#2513 = DIRECTION('',(1.,0.,-0.)); -#2514 = ORIENTED_EDGE('',*,*,#2458,.F.); -#2515 = PLANE('',#2516); -#2516 = AXIS2_PLACEMENT_3D('',#2517,#2518,#2519); -#2517 = CARTESIAN_POINT('',(-41.38888888888,-17.9,8.)); -#2518 = DIRECTION('',(-0.,1.,0.)); -#2519 = DIRECTION('',(0.,0.,1.)); -#2520 = ADVANCED_FACE('',(#2521),#2527,.F.); -#2521 = FACE_BOUND('',#2522,.F.); -#2522 = EDGE_LOOP('',(#2523,#2524,#2525,#2526)); -#2523 = ORIENTED_EDGE('',*,*,#2410,.F.); -#2524 = ORIENTED_EDGE('',*,*,#2480,.T.); -#2525 = ORIENTED_EDGE('',*,*,#2450,.T.); -#2526 = ORIENTED_EDGE('',*,*,#2502,.F.); -#2527 = PLANE('',#2528); -#2528 = AXIS2_PLACEMENT_3D('',#2529,#2530,#2531); -#2529 = CARTESIAN_POINT('',(-41.38888888888,-20.1,8.)); -#2530 = DIRECTION('',(0.,0.,1.)); -#2531 = DIRECTION('',(1.,0.,-0.)); -#2532 = ADVANCED_FACE('',(#2533),#2539,.T.); -#2533 = FACE_BOUND('',#2534,.T.); -#2534 = EDGE_LOOP('',(#2535,#2536,#2537,#2538)); -#2535 = ORIENTED_EDGE('',*,*,#2426,.F.); -#2536 = ORIENTED_EDGE('',*,*,#2487,.T.); -#2537 = ORIENTED_EDGE('',*,*,#2466,.T.); -#2538 = ORIENTED_EDGE('',*,*,#2509,.F.); -#2539 = PLANE('',#2540); -#2540 = AXIS2_PLACEMENT_3D('',#2541,#2542,#2543); -#2541 = CARTESIAN_POINT('',(-41.38888888888,-20.1,40.)); -#2542 = DIRECTION('',(0.,0.,1.)); -#2543 = DIRECTION('',(1.,0.,-0.)); -#2544 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2548)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#2545,#2546,#2547)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#2545 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#2546 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#2547 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#2548 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2545, - 'distance_accuracy_value','confusion accuracy'); -#2549 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2550,#2552); -#2550 = ( REPRESENTATION_RELATIONSHIP('','',#2393,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2551) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#2551 = ITEM_DEFINED_TRANSFORMATION('','',#11,#67); -#2552 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #2553); -#2553 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('14','WireDuct_LeftCombSlot_06', - '',#5,#2388,$); -#2554 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2390)); -#2555 = SHAPE_DEFINITION_REPRESENTATION(#2556,#2562); -#2556 = PRODUCT_DEFINITION_SHAPE('','',#2557); -#2557 = PRODUCT_DEFINITION('design','',#2558,#2561); -#2558 = PRODUCT_DEFINITION_FORMATION('','',#2559); -#2559 = PRODUCT('WireDuct_RightCombSlot_06','WireDuct_RightCombSlot_06', - '',(#2560)); -#2560 = PRODUCT_CONTEXT('',#2,'mechanical'); -#2561 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#2562 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2563),#2713); -#2563 = MANIFOLD_SOLID_BREP('',#2564); -#2564 = CLOSED_SHELL('',(#2565,#2605,#2645,#2667,#2689,#2701)); -#2565 = ADVANCED_FACE('',(#2566),#2600,.F.); -#2566 = FACE_BOUND('',#2567,.F.); -#2567 = EDGE_LOOP('',(#2568,#2578,#2586,#2594)); -#2568 = ORIENTED_EDGE('',*,*,#2569,.F.); -#2569 = EDGE_CURVE('',#2570,#2572,#2574,.T.); -#2570 = VERTEX_POINT('',#2571); -#2571 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2572 = VERTEX_POINT('',#2573); -#2573 = CARTESIAN_POINT('',(-41.38888888888,17.9,40.)); -#2574 = LINE('',#2575,#2576); -#2575 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2576 = VECTOR('',#2577,1.); -#2577 = DIRECTION('',(0.,0.,1.)); -#2578 = ORIENTED_EDGE('',*,*,#2579,.T.); -#2579 = EDGE_CURVE('',#2570,#2580,#2582,.T.); -#2580 = VERTEX_POINT('',#2581); -#2581 = CARTESIAN_POINT('',(-41.38888888888,20.1,8.)); -#2582 = LINE('',#2583,#2584); -#2583 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2584 = VECTOR('',#2585,1.); -#2585 = DIRECTION('',(-0.,1.,0.)); -#2586 = ORIENTED_EDGE('',*,*,#2587,.T.); -#2587 = EDGE_CURVE('',#2580,#2588,#2590,.T.); -#2588 = VERTEX_POINT('',#2589); -#2589 = CARTESIAN_POINT('',(-41.38888888888,20.1,40.)); +#2465 = DIRECTION('',(1.,0.,-0.)); +#2466 = ADVANCED_FACE('',(#2467),#2478,.T.); +#2467 = FACE_BOUND('',#2468,.T.); +#2468 = EDGE_LOOP('',(#2469,#2475,#2476,#2477)); +#2469 = ORIENTED_EDGE('',*,*,#2470,.F.); +#2470 = EDGE_CURVE('',#365,#995,#2471,.T.); +#2471 = LINE('',#2472,#2473); +#2472 = CARTESIAN_POINT('',(-41.38888888888,-20.2,40.)); +#2473 = VECTOR('',#2474,1.); +#2474 = DIRECTION('',(-0.,1.,0.)); +#2475 = ORIENTED_EDGE('',*,*,#372,.T.); +#2476 = ORIENTED_EDGE('',*,*,#2453,.T.); +#2477 = ORIENTED_EDGE('',*,*,#1002,.F.); +#2478 = PLANE('',#2479); +#2479 = AXIS2_PLACEMENT_3D('',#2480,#2481,#2482); +#2480 = CARTESIAN_POINT('',(-41.38888888888,-20.2,8.)); +#2481 = DIRECTION('',(1.,0.,-0.)); +#2482 = DIRECTION('',(0.,0.,1.)); +#2483 = ADVANCED_FACE('',(#2484),#2495,.T.); +#2484 = FACE_BOUND('',#2485,.T.); +#2485 = EDGE_LOOP('',(#2486,#2487,#2493,#2494)); +#2486 = ORIENTED_EDGE('',*,*,#994,.F.); +#2487 = ORIENTED_EDGE('',*,*,#2488,.F.); +#2488 = EDGE_CURVE('',#357,#987,#2489,.T.); +#2489 = LINE('',#2490,#2491); +#2490 = CARTESIAN_POINT('',(-47.5,-20.2,40.)); +#2491 = VECTOR('',#2492,1.); +#2492 = DIRECTION('',(-0.,1.,0.)); +#2493 = ORIENTED_EDGE('',*,*,#364,.T.); +#2494 = ORIENTED_EDGE('',*,*,#2470,.T.); +#2495 = PLANE('',#2496); +#2496 = AXIS2_PLACEMENT_3D('',#2497,#2498,#2499); +#2497 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2498 = DIRECTION('',(0.,0.,1.)); +#2499 = DIRECTION('',(1.,0.,-0.)); +#2500 = ADVANCED_FACE('',(#2501),#2512,.F.); +#2501 = FACE_BOUND('',#2502,.F.); +#2502 = EDGE_LOOP('',(#2503,#2504,#2505,#2511)); +#2503 = ORIENTED_EDGE('',*,*,#2488,.F.); +#2504 = ORIENTED_EDGE('',*,*,#356,.T.); +#2505 = ORIENTED_EDGE('',*,*,#2506,.T.); +#2506 = EDGE_CURVE('',#349,#979,#2507,.T.); +#2507 = LINE('',#2508,#2509); +#2508 = CARTESIAN_POINT('',(-47.5,-20.2,8.)); +#2509 = VECTOR('',#2510,1.); +#2510 = DIRECTION('',(-0.,1.,0.)); +#2511 = ORIENTED_EDGE('',*,*,#986,.F.); +#2512 = PLANE('',#2513); +#2513 = AXIS2_PLACEMENT_3D('',#2514,#2515,#2516); +#2514 = CARTESIAN_POINT('',(-47.5,-20.2,8.)); +#2515 = DIRECTION('',(1.,0.,-0.)); +#2516 = DIRECTION('',(0.,0.,1.)); +#2517 = ADVANCED_FACE('',(#2518),#2529,.T.); +#2518 = FACE_BOUND('',#2519,.T.); +#2519 = EDGE_LOOP('',(#2520,#2526,#2527,#2528)); +#2520 = ORIENTED_EDGE('',*,*,#2521,.F.); +#2521 = EDGE_CURVE('',#341,#971,#2522,.T.); +#2522 = LINE('',#2523,#2524); +#2523 = CARTESIAN_POINT('',(-52.5,-20.2,8.)); +#2524 = VECTOR('',#2525,1.); +#2525 = DIRECTION('',(-0.,1.,0.)); +#2526 = ORIENTED_EDGE('',*,*,#348,.T.); +#2527 = ORIENTED_EDGE('',*,*,#2506,.T.); +#2528 = ORIENTED_EDGE('',*,*,#978,.F.); +#2529 = PLANE('',#2530); +#2530 = AXIS2_PLACEMENT_3D('',#2531,#2532,#2533); +#2531 = CARTESIAN_POINT('',(-52.5,-20.2,8.)); +#2532 = DIRECTION('',(0.,0.,1.)); +#2533 = DIRECTION('',(1.,0.,-0.)); +#2534 = ADVANCED_FACE('',(#2535),#2546,.T.); +#2535 = FACE_BOUND('',#2536,.T.); +#2536 = EDGE_LOOP('',(#2537,#2543,#2544,#2545)); +#2537 = ORIENTED_EDGE('',*,*,#2538,.F.); +#2538 = EDGE_CURVE('',#333,#963,#2539,.T.); +#2539 = LINE('',#2540,#2541); +#2540 = CARTESIAN_POINT('',(-52.5,-20.2,40.)); +#2541 = VECTOR('',#2542,1.); +#2542 = DIRECTION('',(-0.,1.,0.)); +#2543 = ORIENTED_EDGE('',*,*,#340,.T.); +#2544 = ORIENTED_EDGE('',*,*,#2521,.T.); +#2545 = ORIENTED_EDGE('',*,*,#970,.F.); +#2546 = PLANE('',#2547); +#2547 = AXIS2_PLACEMENT_3D('',#2548,#2549,#2550); +#2548 = CARTESIAN_POINT('',(-52.5,-20.2,8.)); +#2549 = DIRECTION('',(1.,0.,-0.)); +#2550 = DIRECTION('',(0.,0.,1.)); +#2551 = ADVANCED_FACE('',(#2552),#2563,.T.); +#2552 = FACE_BOUND('',#2553,.T.); +#2553 = EDGE_LOOP('',(#2554,#2555,#2561,#2562)); +#2554 = ORIENTED_EDGE('',*,*,#962,.F.); +#2555 = ORIENTED_EDGE('',*,*,#2556,.F.); +#2556 = EDGE_CURVE('',#325,#955,#2557,.T.); +#2557 = LINE('',#2558,#2559); +#2558 = CARTESIAN_POINT('',(-58.61111111111,-20.2,40.)); +#2559 = VECTOR('',#2560,1.); +#2560 = DIRECTION('',(-0.,1.,0.)); +#2561 = ORIENTED_EDGE('',*,*,#332,.T.); +#2562 = ORIENTED_EDGE('',*,*,#2538,.T.); +#2563 = PLANE('',#2564); +#2564 = AXIS2_PLACEMENT_3D('',#2565,#2566,#2567); +#2565 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2566 = DIRECTION('',(0.,0.,1.)); +#2567 = DIRECTION('',(1.,0.,-0.)); +#2568 = ADVANCED_FACE('',(#2569),#2580,.F.); +#2569 = FACE_BOUND('',#2570,.F.); +#2570 = EDGE_LOOP('',(#2571,#2572,#2573,#2579)); +#2571 = ORIENTED_EDGE('',*,*,#2556,.F.); +#2572 = ORIENTED_EDGE('',*,*,#324,.T.); +#2573 = ORIENTED_EDGE('',*,*,#2574,.T.); +#2574 = EDGE_CURVE('',#317,#947,#2575,.T.); +#2575 = LINE('',#2576,#2577); +#2576 = CARTESIAN_POINT('',(-58.61111111111,-20.2,8.)); +#2577 = VECTOR('',#2578,1.); +#2578 = DIRECTION('',(-0.,1.,0.)); +#2579 = ORIENTED_EDGE('',*,*,#954,.F.); +#2580 = PLANE('',#2581); +#2581 = AXIS2_PLACEMENT_3D('',#2582,#2583,#2584); +#2582 = CARTESIAN_POINT('',(-58.61111111111,-20.2,8.)); +#2583 = DIRECTION('',(1.,0.,-0.)); +#2584 = DIRECTION('',(0.,0.,1.)); +#2585 = ADVANCED_FACE('',(#2586),#2597,.T.); +#2586 = FACE_BOUND('',#2587,.T.); +#2587 = EDGE_LOOP('',(#2588,#2594,#2595,#2596)); +#2588 = ORIENTED_EDGE('',*,*,#2589,.F.); +#2589 = EDGE_CURVE('',#309,#939,#2590,.T.); #2590 = LINE('',#2591,#2592); -#2591 = CARTESIAN_POINT('',(-41.38888888888,20.1,8.)); +#2591 = CARTESIAN_POINT('',(-63.61111111111,-20.2,8.)); #2592 = VECTOR('',#2593,1.); -#2593 = DIRECTION('',(0.,0.,1.)); -#2594 = ORIENTED_EDGE('',*,*,#2595,.F.); -#2595 = EDGE_CURVE('',#2572,#2588,#2596,.T.); -#2596 = LINE('',#2597,#2598); -#2597 = CARTESIAN_POINT('',(-41.38888888888,17.9,40.)); -#2598 = VECTOR('',#2599,1.); -#2599 = DIRECTION('',(-0.,1.,0.)); -#2600 = PLANE('',#2601); -#2601 = AXIS2_PLACEMENT_3D('',#2602,#2603,#2604); -#2602 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2603 = DIRECTION('',(1.,0.,-0.)); -#2604 = DIRECTION('',(0.,0.,1.)); -#2605 = ADVANCED_FACE('',(#2606),#2640,.T.); -#2606 = FACE_BOUND('',#2607,.T.); -#2607 = EDGE_LOOP('',(#2608,#2618,#2626,#2634)); -#2608 = ORIENTED_EDGE('',*,*,#2609,.F.); -#2609 = EDGE_CURVE('',#2610,#2612,#2614,.T.); -#2610 = VERTEX_POINT('',#2611); -#2611 = CARTESIAN_POINT('',(-36.38888888888,17.9,8.)); -#2612 = VERTEX_POINT('',#2613); -#2613 = CARTESIAN_POINT('',(-36.38888888888,17.9,40.)); -#2614 = LINE('',#2615,#2616); -#2615 = CARTESIAN_POINT('',(-36.38888888888,17.9,8.)); -#2616 = VECTOR('',#2617,1.); -#2617 = DIRECTION('',(0.,0.,1.)); -#2618 = ORIENTED_EDGE('',*,*,#2619,.T.); -#2619 = EDGE_CURVE('',#2610,#2620,#2622,.T.); -#2620 = VERTEX_POINT('',#2621); -#2621 = CARTESIAN_POINT('',(-36.38888888888,20.1,8.)); -#2622 = LINE('',#2623,#2624); -#2623 = CARTESIAN_POINT('',(-36.38888888888,17.9,8.)); -#2624 = VECTOR('',#2625,1.); -#2625 = DIRECTION('',(-0.,1.,0.)); -#2626 = ORIENTED_EDGE('',*,*,#2627,.T.); -#2627 = EDGE_CURVE('',#2620,#2628,#2630,.T.); -#2628 = VERTEX_POINT('',#2629); -#2629 = CARTESIAN_POINT('',(-36.38888888888,20.1,40.)); -#2630 = LINE('',#2631,#2632); -#2631 = CARTESIAN_POINT('',(-36.38888888888,20.1,8.)); -#2632 = VECTOR('',#2633,1.); -#2633 = DIRECTION('',(0.,0.,1.)); -#2634 = ORIENTED_EDGE('',*,*,#2635,.F.); -#2635 = EDGE_CURVE('',#2612,#2628,#2636,.T.); -#2636 = LINE('',#2637,#2638); -#2637 = CARTESIAN_POINT('',(-36.38888888888,17.9,40.)); -#2638 = VECTOR('',#2639,1.); -#2639 = DIRECTION('',(-0.,1.,0.)); -#2640 = PLANE('',#2641); -#2641 = AXIS2_PLACEMENT_3D('',#2642,#2643,#2644); -#2642 = CARTESIAN_POINT('',(-36.38888888888,17.9,8.)); -#2643 = DIRECTION('',(1.,0.,-0.)); -#2644 = DIRECTION('',(0.,0.,1.)); -#2645 = ADVANCED_FACE('',(#2646),#2662,.F.); -#2646 = FACE_BOUND('',#2647,.F.); -#2647 = EDGE_LOOP('',(#2648,#2654,#2655,#2661)); -#2648 = ORIENTED_EDGE('',*,*,#2649,.F.); -#2649 = EDGE_CURVE('',#2570,#2610,#2650,.T.); -#2650 = LINE('',#2651,#2652); -#2651 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2652 = VECTOR('',#2653,1.); -#2653 = DIRECTION('',(1.,0.,-0.)); -#2654 = ORIENTED_EDGE('',*,*,#2569,.T.); -#2655 = ORIENTED_EDGE('',*,*,#2656,.T.); -#2656 = EDGE_CURVE('',#2572,#2612,#2657,.T.); -#2657 = LINE('',#2658,#2659); -#2658 = CARTESIAN_POINT('',(-41.38888888888,17.9,40.)); -#2659 = VECTOR('',#2660,1.); -#2660 = DIRECTION('',(1.,0.,-0.)); -#2661 = ORIENTED_EDGE('',*,*,#2609,.F.); -#2662 = PLANE('',#2663); -#2663 = AXIS2_PLACEMENT_3D('',#2664,#2665,#2666); -#2664 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2665 = DIRECTION('',(-0.,1.,0.)); -#2666 = DIRECTION('',(0.,0.,1.)); -#2667 = ADVANCED_FACE('',(#2668),#2684,.T.); -#2668 = FACE_BOUND('',#2669,.T.); -#2669 = EDGE_LOOP('',(#2670,#2676,#2677,#2683)); -#2670 = ORIENTED_EDGE('',*,*,#2671,.F.); -#2671 = EDGE_CURVE('',#2580,#2620,#2672,.T.); -#2672 = LINE('',#2673,#2674); -#2673 = CARTESIAN_POINT('',(-41.38888888888,20.1,8.)); -#2674 = VECTOR('',#2675,1.); -#2675 = DIRECTION('',(1.,0.,-0.)); -#2676 = ORIENTED_EDGE('',*,*,#2587,.T.); -#2677 = ORIENTED_EDGE('',*,*,#2678,.T.); -#2678 = EDGE_CURVE('',#2588,#2628,#2679,.T.); -#2679 = LINE('',#2680,#2681); -#2680 = CARTESIAN_POINT('',(-41.38888888888,20.1,40.)); -#2681 = VECTOR('',#2682,1.); -#2682 = DIRECTION('',(1.,0.,-0.)); -#2683 = ORIENTED_EDGE('',*,*,#2627,.F.); -#2684 = PLANE('',#2685); -#2685 = AXIS2_PLACEMENT_3D('',#2686,#2687,#2688); -#2686 = CARTESIAN_POINT('',(-41.38888888888,20.1,8.)); -#2687 = DIRECTION('',(-0.,1.,0.)); -#2688 = DIRECTION('',(0.,0.,1.)); -#2689 = ADVANCED_FACE('',(#2690),#2696,.F.); -#2690 = FACE_BOUND('',#2691,.F.); -#2691 = EDGE_LOOP('',(#2692,#2693,#2694,#2695)); -#2692 = ORIENTED_EDGE('',*,*,#2579,.F.); -#2693 = ORIENTED_EDGE('',*,*,#2649,.T.); -#2694 = ORIENTED_EDGE('',*,*,#2619,.T.); -#2695 = ORIENTED_EDGE('',*,*,#2671,.F.); -#2696 = PLANE('',#2697); -#2697 = AXIS2_PLACEMENT_3D('',#2698,#2699,#2700); -#2698 = CARTESIAN_POINT('',(-41.38888888888,17.9,8.)); -#2699 = DIRECTION('',(0.,0.,1.)); -#2700 = DIRECTION('',(1.,0.,-0.)); -#2701 = ADVANCED_FACE('',(#2702),#2708,.T.); -#2702 = FACE_BOUND('',#2703,.T.); -#2703 = EDGE_LOOP('',(#2704,#2705,#2706,#2707)); -#2704 = ORIENTED_EDGE('',*,*,#2595,.F.); -#2705 = ORIENTED_EDGE('',*,*,#2656,.T.); -#2706 = ORIENTED_EDGE('',*,*,#2635,.T.); -#2707 = ORIENTED_EDGE('',*,*,#2678,.F.); -#2708 = PLANE('',#2709); -#2709 = AXIS2_PLACEMENT_3D('',#2710,#2711,#2712); -#2710 = CARTESIAN_POINT('',(-41.38888888888,17.9,40.)); -#2711 = DIRECTION('',(0.,0.,1.)); -#2712 = DIRECTION('',(1.,0.,-0.)); -#2713 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2717)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#2714,#2715,#2716)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#2714 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#2715 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#2716 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#2717 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2714, - 'distance_accuracy_value','confusion accuracy'); -#2718 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2719,#2721); -#2719 = ( REPRESENTATION_RELATIONSHIP('','',#2562,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2720) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#2720 = ITEM_DEFINED_TRANSFORMATION('','',#11,#71); -#2721 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #2722); -#2722 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('15','WireDuct_RightCombSlot_06', - '',#5,#2557,$); -#2723 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2559)); -#2724 = SHAPE_DEFINITION_REPRESENTATION(#2725,#2731); -#2725 = PRODUCT_DEFINITION_SHAPE('','',#2726); -#2726 = PRODUCT_DEFINITION('design','',#2727,#2730); -#2727 = PRODUCT_DEFINITION_FORMATION('','',#2728); -#2728 = PRODUCT('WireDuct_LeftCombSlot_07','WireDuct_LeftCombSlot_07','' - ,(#2729)); -#2729 = PRODUCT_CONTEXT('',#2,'mechanical'); -#2730 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#2731 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2732),#2882); -#2732 = MANIFOLD_SOLID_BREP('',#2733); -#2733 = CLOSED_SHELL('',(#2734,#2774,#2814,#2836,#2858,#2870)); -#2734 = ADVANCED_FACE('',(#2735),#2769,.F.); -#2735 = FACE_BOUND('',#2736,.F.); -#2736 = EDGE_LOOP('',(#2737,#2747,#2755,#2763)); -#2737 = ORIENTED_EDGE('',*,*,#2738,.F.); -#2738 = EDGE_CURVE('',#2739,#2741,#2743,.T.); -#2739 = VERTEX_POINT('',#2740); -#2740 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); -#2741 = VERTEX_POINT('',#2742); -#2742 = CARTESIAN_POINT('',(-30.27777777777,-20.1,40.)); +#2593 = DIRECTION('',(-0.,1.,0.)); +#2594 = ORIENTED_EDGE('',*,*,#316,.T.); +#2595 = ORIENTED_EDGE('',*,*,#2574,.T.); +#2596 = ORIENTED_EDGE('',*,*,#946,.F.); +#2597 = PLANE('',#2598); +#2598 = AXIS2_PLACEMENT_3D('',#2599,#2600,#2601); +#2599 = CARTESIAN_POINT('',(-63.61111111111,-20.2,8.)); +#2600 = DIRECTION('',(0.,0.,1.)); +#2601 = DIRECTION('',(1.,0.,-0.)); +#2602 = ADVANCED_FACE('',(#2603),#2614,.T.); +#2603 = FACE_BOUND('',#2604,.T.); +#2604 = EDGE_LOOP('',(#2605,#2611,#2612,#2613)); +#2605 = ORIENTED_EDGE('',*,*,#2606,.F.); +#2606 = EDGE_CURVE('',#301,#931,#2607,.T.); +#2607 = LINE('',#2608,#2609); +#2608 = CARTESIAN_POINT('',(-63.61111111111,-20.2,40.)); +#2609 = VECTOR('',#2610,1.); +#2610 = DIRECTION('',(-0.,1.,0.)); +#2611 = ORIENTED_EDGE('',*,*,#308,.T.); +#2612 = ORIENTED_EDGE('',*,*,#2589,.T.); +#2613 = ORIENTED_EDGE('',*,*,#938,.F.); +#2614 = PLANE('',#2615); +#2615 = AXIS2_PLACEMENT_3D('',#2616,#2617,#2618); +#2616 = CARTESIAN_POINT('',(-63.61111111111,-20.2,8.)); +#2617 = DIRECTION('',(1.,0.,-0.)); +#2618 = DIRECTION('',(0.,0.,1.)); +#2619 = ADVANCED_FACE('',(#2620),#2631,.T.); +#2620 = FACE_BOUND('',#2621,.T.); +#2621 = EDGE_LOOP('',(#2622,#2623,#2629,#2630)); +#2622 = ORIENTED_EDGE('',*,*,#930,.F.); +#2623 = ORIENTED_EDGE('',*,*,#2624,.F.); +#2624 = EDGE_CURVE('',#293,#923,#2625,.T.); +#2625 = LINE('',#2626,#2627); +#2626 = CARTESIAN_POINT('',(-69.72222222222,-20.2,40.)); +#2627 = VECTOR('',#2628,1.); +#2628 = DIRECTION('',(-0.,1.,0.)); +#2629 = ORIENTED_EDGE('',*,*,#300,.T.); +#2630 = ORIENTED_EDGE('',*,*,#2606,.T.); +#2631 = PLANE('',#2632); +#2632 = AXIS2_PLACEMENT_3D('',#2633,#2634,#2635); +#2633 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2634 = DIRECTION('',(0.,0.,1.)); +#2635 = DIRECTION('',(1.,0.,-0.)); +#2636 = ADVANCED_FACE('',(#2637),#2648,.F.); +#2637 = FACE_BOUND('',#2638,.F.); +#2638 = EDGE_LOOP('',(#2639,#2640,#2641,#2647)); +#2639 = ORIENTED_EDGE('',*,*,#2624,.F.); +#2640 = ORIENTED_EDGE('',*,*,#292,.T.); +#2641 = ORIENTED_EDGE('',*,*,#2642,.T.); +#2642 = EDGE_CURVE('',#285,#915,#2643,.T.); +#2643 = LINE('',#2644,#2645); +#2644 = CARTESIAN_POINT('',(-69.72222222222,-20.2,8.)); +#2645 = VECTOR('',#2646,1.); +#2646 = DIRECTION('',(-0.,1.,0.)); +#2647 = ORIENTED_EDGE('',*,*,#922,.F.); +#2648 = PLANE('',#2649); +#2649 = AXIS2_PLACEMENT_3D('',#2650,#2651,#2652); +#2650 = CARTESIAN_POINT('',(-69.72222222222,-20.2,8.)); +#2651 = DIRECTION('',(1.,0.,-0.)); +#2652 = DIRECTION('',(0.,0.,1.)); +#2653 = ADVANCED_FACE('',(#2654),#2665,.T.); +#2654 = FACE_BOUND('',#2655,.T.); +#2655 = EDGE_LOOP('',(#2656,#2662,#2663,#2664)); +#2656 = ORIENTED_EDGE('',*,*,#2657,.F.); +#2657 = EDGE_CURVE('',#277,#907,#2658,.T.); +#2658 = LINE('',#2659,#2660); +#2659 = CARTESIAN_POINT('',(-74.72222222222,-20.2,8.)); +#2660 = VECTOR('',#2661,1.); +#2661 = DIRECTION('',(-0.,1.,0.)); +#2662 = ORIENTED_EDGE('',*,*,#284,.T.); +#2663 = ORIENTED_EDGE('',*,*,#2642,.T.); +#2664 = ORIENTED_EDGE('',*,*,#914,.F.); +#2665 = PLANE('',#2666); +#2666 = AXIS2_PLACEMENT_3D('',#2667,#2668,#2669); +#2667 = CARTESIAN_POINT('',(-74.72222222222,-20.2,8.)); +#2668 = DIRECTION('',(0.,0.,1.)); +#2669 = DIRECTION('',(1.,0.,-0.)); +#2670 = ADVANCED_FACE('',(#2671),#2682,.T.); +#2671 = FACE_BOUND('',#2672,.T.); +#2672 = EDGE_LOOP('',(#2673,#2679,#2680,#2681)); +#2673 = ORIENTED_EDGE('',*,*,#2674,.F.); +#2674 = EDGE_CURVE('',#269,#899,#2675,.T.); +#2675 = LINE('',#2676,#2677); +#2676 = CARTESIAN_POINT('',(-74.72222222222,-20.2,40.)); +#2677 = VECTOR('',#2678,1.); +#2678 = DIRECTION('',(-0.,1.,0.)); +#2679 = ORIENTED_EDGE('',*,*,#276,.T.); +#2680 = ORIENTED_EDGE('',*,*,#2657,.T.); +#2681 = ORIENTED_EDGE('',*,*,#906,.F.); +#2682 = PLANE('',#2683); +#2683 = AXIS2_PLACEMENT_3D('',#2684,#2685,#2686); +#2684 = CARTESIAN_POINT('',(-74.72222222222,-20.2,8.)); +#2685 = DIRECTION('',(1.,0.,-0.)); +#2686 = DIRECTION('',(0.,0.,1.)); +#2687 = ADVANCED_FACE('',(#2688),#2699,.T.); +#2688 = FACE_BOUND('',#2689,.T.); +#2689 = EDGE_LOOP('',(#2690,#2691,#2697,#2698)); +#2690 = ORIENTED_EDGE('',*,*,#898,.F.); +#2691 = ORIENTED_EDGE('',*,*,#2692,.F.); +#2692 = EDGE_CURVE('',#261,#891,#2693,.T.); +#2693 = LINE('',#2694,#2695); +#2694 = CARTESIAN_POINT('',(-80.83333333333,-20.2,40.)); +#2695 = VECTOR('',#2696,1.); +#2696 = DIRECTION('',(-0.,1.,0.)); +#2697 = ORIENTED_EDGE('',*,*,#268,.T.); +#2698 = ORIENTED_EDGE('',*,*,#2674,.T.); +#2699 = PLANE('',#2700); +#2700 = AXIS2_PLACEMENT_3D('',#2701,#2702,#2703); +#2701 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2702 = DIRECTION('',(0.,0.,1.)); +#2703 = DIRECTION('',(1.,0.,-0.)); +#2704 = ADVANCED_FACE('',(#2705),#2716,.F.); +#2705 = FACE_BOUND('',#2706,.F.); +#2706 = EDGE_LOOP('',(#2707,#2708,#2709,#2715)); +#2707 = ORIENTED_EDGE('',*,*,#2692,.F.); +#2708 = ORIENTED_EDGE('',*,*,#260,.T.); +#2709 = ORIENTED_EDGE('',*,*,#2710,.T.); +#2710 = EDGE_CURVE('',#253,#883,#2711,.T.); +#2711 = LINE('',#2712,#2713); +#2712 = CARTESIAN_POINT('',(-80.83333333333,-20.2,8.)); +#2713 = VECTOR('',#2714,1.); +#2714 = DIRECTION('',(-0.,1.,0.)); +#2715 = ORIENTED_EDGE('',*,*,#890,.F.); +#2716 = PLANE('',#2717); +#2717 = AXIS2_PLACEMENT_3D('',#2718,#2719,#2720); +#2718 = CARTESIAN_POINT('',(-80.83333333333,-20.2,8.)); +#2719 = DIRECTION('',(1.,0.,-0.)); +#2720 = DIRECTION('',(0.,0.,1.)); +#2721 = ADVANCED_FACE('',(#2722),#2733,.T.); +#2722 = FACE_BOUND('',#2723,.T.); +#2723 = EDGE_LOOP('',(#2724,#2730,#2731,#2732)); +#2724 = ORIENTED_EDGE('',*,*,#2725,.F.); +#2725 = EDGE_CURVE('',#245,#875,#2726,.T.); +#2726 = LINE('',#2727,#2728); +#2727 = CARTESIAN_POINT('',(-85.83333333333,-20.2,8.)); +#2728 = VECTOR('',#2729,1.); +#2729 = DIRECTION('',(-0.,1.,0.)); +#2730 = ORIENTED_EDGE('',*,*,#252,.T.); +#2731 = ORIENTED_EDGE('',*,*,#2710,.T.); +#2732 = ORIENTED_EDGE('',*,*,#882,.F.); +#2733 = PLANE('',#2734); +#2734 = AXIS2_PLACEMENT_3D('',#2735,#2736,#2737); +#2735 = CARTESIAN_POINT('',(-85.83333333333,-20.2,8.)); +#2736 = DIRECTION('',(0.,0.,1.)); +#2737 = DIRECTION('',(1.,0.,-0.)); +#2738 = ADVANCED_FACE('',(#2739),#2750,.T.); +#2739 = FACE_BOUND('',#2740,.T.); +#2740 = EDGE_LOOP('',(#2741,#2747,#2748,#2749)); +#2741 = ORIENTED_EDGE('',*,*,#2742,.F.); +#2742 = EDGE_CURVE('',#237,#867,#2743,.T.); #2743 = LINE('',#2744,#2745); -#2744 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); +#2744 = CARTESIAN_POINT('',(-85.83333333333,-20.2,40.)); #2745 = VECTOR('',#2746,1.); -#2746 = DIRECTION('',(0.,0.,1.)); -#2747 = ORIENTED_EDGE('',*,*,#2748,.T.); -#2748 = EDGE_CURVE('',#2739,#2749,#2751,.T.); -#2749 = VERTEX_POINT('',#2750); -#2750 = CARTESIAN_POINT('',(-30.27777777777,-17.9,8.)); -#2751 = LINE('',#2752,#2753); -#2752 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); -#2753 = VECTOR('',#2754,1.); -#2754 = DIRECTION('',(-0.,1.,0.)); -#2755 = ORIENTED_EDGE('',*,*,#2756,.T.); -#2756 = EDGE_CURVE('',#2749,#2757,#2759,.T.); -#2757 = VERTEX_POINT('',#2758); -#2758 = CARTESIAN_POINT('',(-30.27777777777,-17.9,40.)); -#2759 = LINE('',#2760,#2761); -#2760 = CARTESIAN_POINT('',(-30.27777777777,-17.9,8.)); -#2761 = VECTOR('',#2762,1.); -#2762 = DIRECTION('',(0.,0.,1.)); -#2763 = ORIENTED_EDGE('',*,*,#2764,.F.); -#2764 = EDGE_CURVE('',#2741,#2757,#2765,.T.); -#2765 = LINE('',#2766,#2767); -#2766 = CARTESIAN_POINT('',(-30.27777777777,-20.1,40.)); -#2767 = VECTOR('',#2768,1.); -#2768 = DIRECTION('',(-0.,1.,0.)); -#2769 = PLANE('',#2770); -#2770 = AXIS2_PLACEMENT_3D('',#2771,#2772,#2773); -#2771 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); -#2772 = DIRECTION('',(1.,0.,-0.)); -#2773 = DIRECTION('',(0.,0.,1.)); -#2774 = ADVANCED_FACE('',(#2775),#2809,.T.); -#2775 = FACE_BOUND('',#2776,.T.); -#2776 = EDGE_LOOP('',(#2777,#2787,#2795,#2803)); -#2777 = ORIENTED_EDGE('',*,*,#2778,.F.); -#2778 = EDGE_CURVE('',#2779,#2781,#2783,.T.); -#2779 = VERTEX_POINT('',#2780); -#2780 = CARTESIAN_POINT('',(-25.27777777777,-20.1,8.)); -#2781 = VERTEX_POINT('',#2782); -#2782 = CARTESIAN_POINT('',(-25.27777777777,-20.1,40.)); -#2783 = LINE('',#2784,#2785); -#2784 = CARTESIAN_POINT('',(-25.27777777777,-20.1,8.)); -#2785 = VECTOR('',#2786,1.); -#2786 = DIRECTION('',(0.,0.,1.)); -#2787 = ORIENTED_EDGE('',*,*,#2788,.T.); -#2788 = EDGE_CURVE('',#2779,#2789,#2791,.T.); -#2789 = VERTEX_POINT('',#2790); -#2790 = CARTESIAN_POINT('',(-25.27777777777,-17.9,8.)); -#2791 = LINE('',#2792,#2793); -#2792 = CARTESIAN_POINT('',(-25.27777777777,-20.1,8.)); -#2793 = VECTOR('',#2794,1.); -#2794 = DIRECTION('',(-0.,1.,0.)); -#2795 = ORIENTED_EDGE('',*,*,#2796,.T.); -#2796 = EDGE_CURVE('',#2789,#2797,#2799,.T.); -#2797 = VERTEX_POINT('',#2798); -#2798 = CARTESIAN_POINT('',(-25.27777777777,-17.9,40.)); -#2799 = LINE('',#2800,#2801); -#2800 = CARTESIAN_POINT('',(-25.27777777777,-17.9,8.)); -#2801 = VECTOR('',#2802,1.); -#2802 = DIRECTION('',(0.,0.,1.)); -#2803 = ORIENTED_EDGE('',*,*,#2804,.F.); -#2804 = EDGE_CURVE('',#2781,#2797,#2805,.T.); -#2805 = LINE('',#2806,#2807); -#2806 = CARTESIAN_POINT('',(-25.27777777777,-20.1,40.)); -#2807 = VECTOR('',#2808,1.); -#2808 = DIRECTION('',(-0.,1.,0.)); -#2809 = PLANE('',#2810); -#2810 = AXIS2_PLACEMENT_3D('',#2811,#2812,#2813); -#2811 = CARTESIAN_POINT('',(-25.27777777777,-20.1,8.)); -#2812 = DIRECTION('',(1.,0.,-0.)); -#2813 = DIRECTION('',(0.,0.,1.)); -#2814 = ADVANCED_FACE('',(#2815),#2831,.F.); -#2815 = FACE_BOUND('',#2816,.F.); -#2816 = EDGE_LOOP('',(#2817,#2823,#2824,#2830)); -#2817 = ORIENTED_EDGE('',*,*,#2818,.F.); -#2818 = EDGE_CURVE('',#2739,#2779,#2819,.T.); -#2819 = LINE('',#2820,#2821); -#2820 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); -#2821 = VECTOR('',#2822,1.); -#2822 = DIRECTION('',(1.,0.,-0.)); -#2823 = ORIENTED_EDGE('',*,*,#2738,.T.); -#2824 = ORIENTED_EDGE('',*,*,#2825,.T.); -#2825 = EDGE_CURVE('',#2741,#2781,#2826,.T.); -#2826 = LINE('',#2827,#2828); -#2827 = CARTESIAN_POINT('',(-30.27777777777,-20.1,40.)); -#2828 = VECTOR('',#2829,1.); -#2829 = DIRECTION('',(1.,0.,-0.)); -#2830 = ORIENTED_EDGE('',*,*,#2778,.F.); -#2831 = PLANE('',#2832); -#2832 = AXIS2_PLACEMENT_3D('',#2833,#2834,#2835); -#2833 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); -#2834 = DIRECTION('',(-0.,1.,0.)); -#2835 = DIRECTION('',(0.,0.,1.)); -#2836 = ADVANCED_FACE('',(#2837),#2853,.T.); -#2837 = FACE_BOUND('',#2838,.T.); -#2838 = EDGE_LOOP('',(#2839,#2845,#2846,#2852)); -#2839 = ORIENTED_EDGE('',*,*,#2840,.F.); -#2840 = EDGE_CURVE('',#2749,#2789,#2841,.T.); -#2841 = LINE('',#2842,#2843); -#2842 = CARTESIAN_POINT('',(-30.27777777777,-17.9,8.)); -#2843 = VECTOR('',#2844,1.); -#2844 = DIRECTION('',(1.,0.,-0.)); -#2845 = ORIENTED_EDGE('',*,*,#2756,.T.); +#2746 = DIRECTION('',(-0.,1.,0.)); +#2747 = ORIENTED_EDGE('',*,*,#244,.T.); +#2748 = ORIENTED_EDGE('',*,*,#2725,.T.); +#2749 = ORIENTED_EDGE('',*,*,#874,.F.); +#2750 = PLANE('',#2751); +#2751 = AXIS2_PLACEMENT_3D('',#2752,#2753,#2754); +#2752 = CARTESIAN_POINT('',(-85.83333333333,-20.2,8.)); +#2753 = DIRECTION('',(1.,0.,-0.)); +#2754 = DIRECTION('',(0.,0.,1.)); +#2755 = ADVANCED_FACE('',(#2756),#2767,.T.); +#2756 = FACE_BOUND('',#2757,.T.); +#2757 = EDGE_LOOP('',(#2758,#2759,#2765,#2766)); +#2758 = ORIENTED_EDGE('',*,*,#866,.F.); +#2759 = ORIENTED_EDGE('',*,*,#2760,.F.); +#2760 = EDGE_CURVE('',#229,#859,#2761,.T.); +#2761 = LINE('',#2762,#2763); +#2762 = CARTESIAN_POINT('',(-91.94444444444,-20.2,40.)); +#2763 = VECTOR('',#2764,1.); +#2764 = DIRECTION('',(-0.,1.,0.)); +#2765 = ORIENTED_EDGE('',*,*,#236,.T.); +#2766 = ORIENTED_EDGE('',*,*,#2742,.T.); +#2767 = PLANE('',#2768); +#2768 = AXIS2_PLACEMENT_3D('',#2769,#2770,#2771); +#2769 = CARTESIAN_POINT('',(-100.,-20.,40.)); +#2770 = DIRECTION('',(0.,0.,1.)); +#2771 = DIRECTION('',(1.,0.,-0.)); +#2772 = ADVANCED_FACE('',(#2773),#2784,.F.); +#2773 = FACE_BOUND('',#2774,.F.); +#2774 = EDGE_LOOP('',(#2775,#2776,#2777,#2783)); +#2775 = ORIENTED_EDGE('',*,*,#2760,.F.); +#2776 = ORIENTED_EDGE('',*,*,#228,.T.); +#2777 = ORIENTED_EDGE('',*,*,#2778,.T.); +#2778 = EDGE_CURVE('',#221,#851,#2779,.T.); +#2779 = LINE('',#2780,#2781); +#2780 = CARTESIAN_POINT('',(-91.94444444444,-20.2,8.)); +#2781 = VECTOR('',#2782,1.); +#2782 = DIRECTION('',(-0.,1.,0.)); +#2783 = ORIENTED_EDGE('',*,*,#858,.F.); +#2784 = PLANE('',#2785); +#2785 = AXIS2_PLACEMENT_3D('',#2786,#2787,#2788); +#2786 = CARTESIAN_POINT('',(-91.94444444444,-20.2,8.)); +#2787 = DIRECTION('',(1.,0.,-0.)); +#2788 = DIRECTION('',(0.,0.,1.)); +#2789 = ADVANCED_FACE('',(#2790),#2801,.T.); +#2790 = FACE_BOUND('',#2791,.T.); +#2791 = EDGE_LOOP('',(#2792,#2798,#2799,#2800)); +#2792 = ORIENTED_EDGE('',*,*,#2793,.F.); +#2793 = EDGE_CURVE('',#213,#843,#2794,.T.); +#2794 = LINE('',#2795,#2796); +#2795 = CARTESIAN_POINT('',(-96.94444444444,-20.2,8.)); +#2796 = VECTOR('',#2797,1.); +#2797 = DIRECTION('',(-0.,1.,0.)); +#2798 = ORIENTED_EDGE('',*,*,#220,.T.); +#2799 = ORIENTED_EDGE('',*,*,#2778,.T.); +#2800 = ORIENTED_EDGE('',*,*,#850,.F.); +#2801 = PLANE('',#2802); +#2802 = AXIS2_PLACEMENT_3D('',#2803,#2804,#2805); +#2803 = CARTESIAN_POINT('',(-96.94444444444,-20.2,8.)); +#2804 = DIRECTION('',(0.,0.,1.)); +#2805 = DIRECTION('',(1.,0.,-0.)); +#2806 = ADVANCED_FACE('',(#2807),#2813,.T.); +#2807 = FACE_BOUND('',#2808,.T.); +#2808 = EDGE_LOOP('',(#2809,#2810,#2811,#2812)); +#2809 = ORIENTED_EDGE('',*,*,#812,.F.); +#2810 = ORIENTED_EDGE('',*,*,#212,.T.); +#2811 = ORIENTED_EDGE('',*,*,#2793,.T.); +#2812 = ORIENTED_EDGE('',*,*,#842,.F.); +#2813 = PLANE('',#2814); +#2814 = AXIS2_PLACEMENT_3D('',#2815,#2816,#2817); +#2815 = CARTESIAN_POINT('',(-96.94444444444,-20.2,8.)); +#2816 = DIRECTION('',(1.,0.,-0.)); +#2817 = DIRECTION('',(0.,0.,1.)); +#2818 = ADVANCED_FACE('',(#2819),#3420,.F.); +#2819 = FACE_BOUND('',#2820,.F.); +#2820 = EDGE_LOOP('',(#2821,#2829,#2830,#2838,#2846,#2854,#2862,#2870, + #2878,#2886,#2894,#2902,#2910,#2918,#2926,#2934,#2942,#2950,#2958, + #2966,#2974,#2982,#2990,#2998,#3006,#3014,#3022,#3030,#3038,#3046, + #3054,#3062,#3070,#3078,#3086,#3094,#3102,#3110,#3118,#3126,#3134, + #3142,#3150,#3158,#3166,#3174,#3182,#3190,#3198,#3206,#3214,#3222, + #3230,#3238,#3246,#3254,#3262,#3270,#3278,#3286,#3294,#3302,#3310, + #3318,#3326,#3334,#3342,#3350,#3358,#3366,#3374,#3382,#3390,#3398, + #3406,#3414)); +#2821 = ORIENTED_EDGE('',*,*,#2822,.F.); +#2822 = EDGE_CURVE('',#1427,#2823,#2825,.T.); +#2823 = VERTEX_POINT('',#2824); +#2824 = CARTESIAN_POINT('',(100.,18.,40.)); +#2825 = LINE('',#2826,#2827); +#2826 = CARTESIAN_POINT('',(100.,18.,0.)); +#2827 = VECTOR('',#2828,1.); +#2828 = DIRECTION('',(0.,0.,1.)); +#2829 = ORIENTED_EDGE('',*,*,#1434,.T.); +#2830 = ORIENTED_EDGE('',*,*,#2831,.T.); +#2831 = EDGE_CURVE('',#133,#2832,#2834,.T.); +#2832 = VERTEX_POINT('',#2833); +#2833 = CARTESIAN_POINT('',(-100.,18.,40.)); +#2834 = LINE('',#2835,#2836); +#2835 = CARTESIAN_POINT('',(-100.,18.,0.)); +#2836 = VECTOR('',#2837,1.); +#2837 = DIRECTION('',(0.,0.,1.)); +#2838 = ORIENTED_EDGE('',*,*,#2839,.T.); +#2839 = EDGE_CURVE('',#2832,#2840,#2842,.T.); +#2840 = VERTEX_POINT('',#2841); +#2841 = CARTESIAN_POINT('',(-96.94444444444,18.,40.)); +#2842 = LINE('',#2843,#2844); +#2843 = CARTESIAN_POINT('',(-100.,18.,40.)); +#2844 = VECTOR('',#2845,1.); +#2845 = DIRECTION('',(1.,0.,-0.)); #2846 = ORIENTED_EDGE('',*,*,#2847,.T.); -#2847 = EDGE_CURVE('',#2757,#2797,#2848,.T.); -#2848 = LINE('',#2849,#2850); -#2849 = CARTESIAN_POINT('',(-30.27777777777,-17.9,40.)); -#2850 = VECTOR('',#2851,1.); -#2851 = DIRECTION('',(1.,0.,-0.)); -#2852 = ORIENTED_EDGE('',*,*,#2796,.F.); -#2853 = PLANE('',#2854); -#2854 = AXIS2_PLACEMENT_3D('',#2855,#2856,#2857); -#2855 = CARTESIAN_POINT('',(-30.27777777777,-17.9,8.)); -#2856 = DIRECTION('',(-0.,1.,0.)); -#2857 = DIRECTION('',(0.,0.,1.)); -#2858 = ADVANCED_FACE('',(#2859),#2865,.F.); -#2859 = FACE_BOUND('',#2860,.F.); -#2860 = EDGE_LOOP('',(#2861,#2862,#2863,#2864)); -#2861 = ORIENTED_EDGE('',*,*,#2748,.F.); -#2862 = ORIENTED_EDGE('',*,*,#2818,.T.); -#2863 = ORIENTED_EDGE('',*,*,#2788,.T.); -#2864 = ORIENTED_EDGE('',*,*,#2840,.F.); -#2865 = PLANE('',#2866); -#2866 = AXIS2_PLACEMENT_3D('',#2867,#2868,#2869); -#2867 = CARTESIAN_POINT('',(-30.27777777777,-20.1,8.)); -#2868 = DIRECTION('',(0.,0.,1.)); -#2869 = DIRECTION('',(1.,0.,-0.)); -#2870 = ADVANCED_FACE('',(#2871),#2877,.T.); -#2871 = FACE_BOUND('',#2872,.T.); -#2872 = EDGE_LOOP('',(#2873,#2874,#2875,#2876)); -#2873 = ORIENTED_EDGE('',*,*,#2764,.F.); -#2874 = ORIENTED_EDGE('',*,*,#2825,.T.); -#2875 = ORIENTED_EDGE('',*,*,#2804,.T.); -#2876 = ORIENTED_EDGE('',*,*,#2847,.F.); -#2877 = PLANE('',#2878); -#2878 = AXIS2_PLACEMENT_3D('',#2879,#2880,#2881); -#2879 = CARTESIAN_POINT('',(-30.27777777777,-20.1,40.)); -#2880 = DIRECTION('',(0.,0.,1.)); -#2881 = DIRECTION('',(1.,0.,-0.)); -#2882 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2886)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#2883,#2884,#2885)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#2883 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#2884 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#2885 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#2886 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2883, - 'distance_accuracy_value','confusion accuracy'); -#2887 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2888,#2890); -#2888 = ( REPRESENTATION_RELATIONSHIP('','',#2731,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2889) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#2889 = ITEM_DEFINED_TRANSFORMATION('','',#11,#75); -#2890 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #2891); -#2891 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('16','WireDuct_LeftCombSlot_07', - '',#5,#2726,$); -#2892 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2728)); -#2893 = SHAPE_DEFINITION_REPRESENTATION(#2894,#2900); -#2894 = PRODUCT_DEFINITION_SHAPE('','',#2895); -#2895 = PRODUCT_DEFINITION('design','',#2896,#2899); -#2896 = PRODUCT_DEFINITION_FORMATION('','',#2897); -#2897 = PRODUCT('WireDuct_RightCombSlot_07','WireDuct_RightCombSlot_07', - '',(#2898)); -#2898 = PRODUCT_CONTEXT('',#2,'mechanical'); -#2899 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#2900 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2901),#3051); -#2901 = MANIFOLD_SOLID_BREP('',#2902); -#2902 = CLOSED_SHELL('',(#2903,#2943,#2983,#3005,#3027,#3039)); -#2903 = ADVANCED_FACE('',(#2904),#2938,.F.); -#2904 = FACE_BOUND('',#2905,.F.); -#2905 = EDGE_LOOP('',(#2906,#2916,#2924,#2932)); -#2906 = ORIENTED_EDGE('',*,*,#2907,.F.); -#2907 = EDGE_CURVE('',#2908,#2910,#2912,.T.); -#2908 = VERTEX_POINT('',#2909); -#2909 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); -#2910 = VERTEX_POINT('',#2911); -#2911 = CARTESIAN_POINT('',(-30.27777777777,17.9,40.)); -#2912 = LINE('',#2913,#2914); -#2913 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); -#2914 = VECTOR('',#2915,1.); -#2915 = DIRECTION('',(0.,0.,1.)); -#2916 = ORIENTED_EDGE('',*,*,#2917,.T.); -#2917 = EDGE_CURVE('',#2908,#2918,#2920,.T.); -#2918 = VERTEX_POINT('',#2919); -#2919 = CARTESIAN_POINT('',(-30.27777777777,20.1,8.)); -#2920 = LINE('',#2921,#2922); -#2921 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); -#2922 = VECTOR('',#2923,1.); -#2923 = DIRECTION('',(-0.,1.,0.)); -#2924 = ORIENTED_EDGE('',*,*,#2925,.T.); -#2925 = EDGE_CURVE('',#2918,#2926,#2928,.T.); -#2926 = VERTEX_POINT('',#2927); -#2927 = CARTESIAN_POINT('',(-30.27777777777,20.1,40.)); -#2928 = LINE('',#2929,#2930); -#2929 = CARTESIAN_POINT('',(-30.27777777777,20.1,8.)); -#2930 = VECTOR('',#2931,1.); -#2931 = DIRECTION('',(0.,0.,1.)); -#2932 = ORIENTED_EDGE('',*,*,#2933,.F.); -#2933 = EDGE_CURVE('',#2910,#2926,#2934,.T.); -#2934 = LINE('',#2935,#2936); -#2935 = CARTESIAN_POINT('',(-30.27777777777,17.9,40.)); -#2936 = VECTOR('',#2937,1.); -#2937 = DIRECTION('',(-0.,1.,0.)); -#2938 = PLANE('',#2939); -#2939 = AXIS2_PLACEMENT_3D('',#2940,#2941,#2942); -#2940 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); +#2847 = EDGE_CURVE('',#2840,#2848,#2850,.T.); +#2848 = VERTEX_POINT('',#2849); +#2849 = CARTESIAN_POINT('',(-96.94444444444,18.,8.)); +#2850 = LINE('',#2851,#2852); +#2851 = CARTESIAN_POINT('',(-96.94444444444,18.,4.)); +#2852 = VECTOR('',#2853,1.); +#2853 = DIRECTION('',(-0.,0.,-1.)); +#2854 = ORIENTED_EDGE('',*,*,#2855,.T.); +#2855 = EDGE_CURVE('',#2848,#2856,#2858,.T.); +#2856 = VERTEX_POINT('',#2857); +#2857 = CARTESIAN_POINT('',(-91.94444444444,18.,8.)); +#2858 = LINE('',#2859,#2860); +#2859 = CARTESIAN_POINT('',(-98.47222222222,18.,8.)); +#2860 = VECTOR('',#2861,1.); +#2861 = DIRECTION('',(1.,0.,-0.)); +#2862 = ORIENTED_EDGE('',*,*,#2863,.F.); +#2863 = EDGE_CURVE('',#2864,#2856,#2866,.T.); +#2864 = VERTEX_POINT('',#2865); +#2865 = CARTESIAN_POINT('',(-91.94444444444,18.,40.)); +#2866 = LINE('',#2867,#2868); +#2867 = CARTESIAN_POINT('',(-91.94444444444,18.,4.)); +#2868 = VECTOR('',#2869,1.); +#2869 = DIRECTION('',(-0.,0.,-1.)); +#2870 = ORIENTED_EDGE('',*,*,#2871,.T.); +#2871 = EDGE_CURVE('',#2864,#2872,#2874,.T.); +#2872 = VERTEX_POINT('',#2873); +#2873 = CARTESIAN_POINT('',(-85.83333333333,18.,40.)); +#2874 = LINE('',#2875,#2876); +#2875 = CARTESIAN_POINT('',(-100.,18.,40.)); +#2876 = VECTOR('',#2877,1.); +#2877 = DIRECTION('',(1.,0.,-0.)); +#2878 = ORIENTED_EDGE('',*,*,#2879,.T.); +#2879 = EDGE_CURVE('',#2872,#2880,#2882,.T.); +#2880 = VERTEX_POINT('',#2881); +#2881 = CARTESIAN_POINT('',(-85.83333333333,18.,8.)); +#2882 = LINE('',#2883,#2884); +#2883 = CARTESIAN_POINT('',(-85.83333333333,18.,4.)); +#2884 = VECTOR('',#2885,1.); +#2885 = DIRECTION('',(-0.,0.,-1.)); +#2886 = ORIENTED_EDGE('',*,*,#2887,.T.); +#2887 = EDGE_CURVE('',#2880,#2888,#2890,.T.); +#2888 = VERTEX_POINT('',#2889); +#2889 = CARTESIAN_POINT('',(-80.83333333333,18.,8.)); +#2890 = LINE('',#2891,#2892); +#2891 = CARTESIAN_POINT('',(-92.91666666666,18.,8.)); +#2892 = VECTOR('',#2893,1.); +#2893 = DIRECTION('',(1.,0.,-0.)); +#2894 = ORIENTED_EDGE('',*,*,#2895,.F.); +#2895 = EDGE_CURVE('',#2896,#2888,#2898,.T.); +#2896 = VERTEX_POINT('',#2897); +#2897 = CARTESIAN_POINT('',(-80.83333333333,18.,40.)); +#2898 = LINE('',#2899,#2900); +#2899 = CARTESIAN_POINT('',(-80.83333333333,18.,4.)); +#2900 = VECTOR('',#2901,1.); +#2901 = DIRECTION('',(-0.,0.,-1.)); +#2902 = ORIENTED_EDGE('',*,*,#2903,.T.); +#2903 = EDGE_CURVE('',#2896,#2904,#2906,.T.); +#2904 = VERTEX_POINT('',#2905); +#2905 = CARTESIAN_POINT('',(-74.72222222222,18.,40.)); +#2906 = LINE('',#2907,#2908); +#2907 = CARTESIAN_POINT('',(-100.,18.,40.)); +#2908 = VECTOR('',#2909,1.); +#2909 = DIRECTION('',(1.,0.,-0.)); +#2910 = ORIENTED_EDGE('',*,*,#2911,.T.); +#2911 = EDGE_CURVE('',#2904,#2912,#2914,.T.); +#2912 = VERTEX_POINT('',#2913); +#2913 = CARTESIAN_POINT('',(-74.72222222222,18.,8.)); +#2914 = LINE('',#2915,#2916); +#2915 = CARTESIAN_POINT('',(-74.72222222222,18.,4.)); +#2916 = VECTOR('',#2917,1.); +#2917 = DIRECTION('',(-0.,0.,-1.)); +#2918 = ORIENTED_EDGE('',*,*,#2919,.T.); +#2919 = EDGE_CURVE('',#2912,#2920,#2922,.T.); +#2920 = VERTEX_POINT('',#2921); +#2921 = CARTESIAN_POINT('',(-69.72222222222,18.,8.)); +#2922 = LINE('',#2923,#2924); +#2923 = CARTESIAN_POINT('',(-87.36111111111,18.,8.)); +#2924 = VECTOR('',#2925,1.); +#2925 = DIRECTION('',(1.,0.,-0.)); +#2926 = ORIENTED_EDGE('',*,*,#2927,.F.); +#2927 = EDGE_CURVE('',#2928,#2920,#2930,.T.); +#2928 = VERTEX_POINT('',#2929); +#2929 = CARTESIAN_POINT('',(-69.72222222222,18.,40.)); +#2930 = LINE('',#2931,#2932); +#2931 = CARTESIAN_POINT('',(-69.72222222222,18.,4.)); +#2932 = VECTOR('',#2933,1.); +#2933 = DIRECTION('',(-0.,0.,-1.)); +#2934 = ORIENTED_EDGE('',*,*,#2935,.T.); +#2935 = EDGE_CURVE('',#2928,#2936,#2938,.T.); +#2936 = VERTEX_POINT('',#2937); +#2937 = CARTESIAN_POINT('',(-63.61111111111,18.,40.)); +#2938 = LINE('',#2939,#2940); +#2939 = CARTESIAN_POINT('',(-100.,18.,40.)); +#2940 = VECTOR('',#2941,1.); #2941 = DIRECTION('',(1.,0.,-0.)); -#2942 = DIRECTION('',(0.,0.,1.)); -#2943 = ADVANCED_FACE('',(#2944),#2978,.T.); -#2944 = FACE_BOUND('',#2945,.T.); -#2945 = EDGE_LOOP('',(#2946,#2956,#2964,#2972)); -#2946 = ORIENTED_EDGE('',*,*,#2947,.F.); -#2947 = EDGE_CURVE('',#2948,#2950,#2952,.T.); -#2948 = VERTEX_POINT('',#2949); -#2949 = CARTESIAN_POINT('',(-25.27777777777,17.9,8.)); -#2950 = VERTEX_POINT('',#2951); -#2951 = CARTESIAN_POINT('',(-25.27777777777,17.9,40.)); -#2952 = LINE('',#2953,#2954); -#2953 = CARTESIAN_POINT('',(-25.27777777777,17.9,8.)); -#2954 = VECTOR('',#2955,1.); -#2955 = DIRECTION('',(0.,0.,1.)); -#2956 = ORIENTED_EDGE('',*,*,#2957,.T.); -#2957 = EDGE_CURVE('',#2948,#2958,#2960,.T.); -#2958 = VERTEX_POINT('',#2959); -#2959 = CARTESIAN_POINT('',(-25.27777777777,20.1,8.)); -#2960 = LINE('',#2961,#2962); -#2961 = CARTESIAN_POINT('',(-25.27777777777,17.9,8.)); -#2962 = VECTOR('',#2963,1.); -#2963 = DIRECTION('',(-0.,1.,0.)); -#2964 = ORIENTED_EDGE('',*,*,#2965,.T.); -#2965 = EDGE_CURVE('',#2958,#2966,#2968,.T.); -#2966 = VERTEX_POINT('',#2967); -#2967 = CARTESIAN_POINT('',(-25.27777777777,20.1,40.)); -#2968 = LINE('',#2969,#2970); -#2969 = CARTESIAN_POINT('',(-25.27777777777,20.1,8.)); -#2970 = VECTOR('',#2971,1.); -#2971 = DIRECTION('',(0.,0.,1.)); -#2972 = ORIENTED_EDGE('',*,*,#2973,.F.); -#2973 = EDGE_CURVE('',#2950,#2966,#2974,.T.); -#2974 = LINE('',#2975,#2976); -#2975 = CARTESIAN_POINT('',(-25.27777777777,17.9,40.)); -#2976 = VECTOR('',#2977,1.); -#2977 = DIRECTION('',(-0.,1.,0.)); -#2978 = PLANE('',#2979); -#2979 = AXIS2_PLACEMENT_3D('',#2980,#2981,#2982); -#2980 = CARTESIAN_POINT('',(-25.27777777777,17.9,8.)); -#2981 = DIRECTION('',(1.,0.,-0.)); -#2982 = DIRECTION('',(0.,0.,1.)); -#2983 = ADVANCED_FACE('',(#2984),#3000,.F.); -#2984 = FACE_BOUND('',#2985,.F.); -#2985 = EDGE_LOOP('',(#2986,#2992,#2993,#2999)); -#2986 = ORIENTED_EDGE('',*,*,#2987,.F.); -#2987 = EDGE_CURVE('',#2908,#2948,#2988,.T.); -#2988 = LINE('',#2989,#2990); -#2989 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); -#2990 = VECTOR('',#2991,1.); -#2991 = DIRECTION('',(1.,0.,-0.)); -#2992 = ORIENTED_EDGE('',*,*,#2907,.T.); -#2993 = ORIENTED_EDGE('',*,*,#2994,.T.); -#2994 = EDGE_CURVE('',#2910,#2950,#2995,.T.); -#2995 = LINE('',#2996,#2997); -#2996 = CARTESIAN_POINT('',(-30.27777777777,17.9,40.)); -#2997 = VECTOR('',#2998,1.); -#2998 = DIRECTION('',(1.,0.,-0.)); -#2999 = ORIENTED_EDGE('',*,*,#2947,.F.); -#3000 = PLANE('',#3001); -#3001 = AXIS2_PLACEMENT_3D('',#3002,#3003,#3004); -#3002 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); -#3003 = DIRECTION('',(-0.,1.,0.)); -#3004 = DIRECTION('',(0.,0.,1.)); -#3005 = ADVANCED_FACE('',(#3006),#3022,.T.); -#3006 = FACE_BOUND('',#3007,.T.); -#3007 = EDGE_LOOP('',(#3008,#3014,#3015,#3021)); -#3008 = ORIENTED_EDGE('',*,*,#3009,.F.); -#3009 = EDGE_CURVE('',#2918,#2958,#3010,.T.); +#2942 = ORIENTED_EDGE('',*,*,#2943,.T.); +#2943 = EDGE_CURVE('',#2936,#2944,#2946,.T.); +#2944 = VERTEX_POINT('',#2945); +#2945 = CARTESIAN_POINT('',(-63.61111111111,18.,8.)); +#2946 = LINE('',#2947,#2948); +#2947 = CARTESIAN_POINT('',(-63.61111111111,18.,4.)); +#2948 = VECTOR('',#2949,1.); +#2949 = DIRECTION('',(-0.,0.,-1.)); +#2950 = ORIENTED_EDGE('',*,*,#2951,.T.); +#2951 = EDGE_CURVE('',#2944,#2952,#2954,.T.); +#2952 = VERTEX_POINT('',#2953); +#2953 = CARTESIAN_POINT('',(-58.61111111111,18.,8.)); +#2954 = LINE('',#2955,#2956); +#2955 = CARTESIAN_POINT('',(-81.80555555555,18.,8.)); +#2956 = VECTOR('',#2957,1.); +#2957 = DIRECTION('',(1.,0.,-0.)); +#2958 = ORIENTED_EDGE('',*,*,#2959,.F.); +#2959 = EDGE_CURVE('',#2960,#2952,#2962,.T.); +#2960 = VERTEX_POINT('',#2961); +#2961 = CARTESIAN_POINT('',(-58.61111111111,18.,40.)); +#2962 = LINE('',#2963,#2964); +#2963 = CARTESIAN_POINT('',(-58.61111111111,18.,4.)); +#2964 = VECTOR('',#2965,1.); +#2965 = DIRECTION('',(-0.,0.,-1.)); +#2966 = ORIENTED_EDGE('',*,*,#2967,.T.); +#2967 = EDGE_CURVE('',#2960,#2968,#2970,.T.); +#2968 = VERTEX_POINT('',#2969); +#2969 = CARTESIAN_POINT('',(-52.5,18.,40.)); +#2970 = LINE('',#2971,#2972); +#2971 = CARTESIAN_POINT('',(-100.,18.,40.)); +#2972 = VECTOR('',#2973,1.); +#2973 = DIRECTION('',(1.,0.,-0.)); +#2974 = ORIENTED_EDGE('',*,*,#2975,.T.); +#2975 = EDGE_CURVE('',#2968,#2976,#2978,.T.); +#2976 = VERTEX_POINT('',#2977); +#2977 = CARTESIAN_POINT('',(-52.5,18.,8.)); +#2978 = LINE('',#2979,#2980); +#2979 = CARTESIAN_POINT('',(-52.5,18.,4.)); +#2980 = VECTOR('',#2981,1.); +#2981 = DIRECTION('',(-0.,0.,-1.)); +#2982 = ORIENTED_EDGE('',*,*,#2983,.T.); +#2983 = EDGE_CURVE('',#2976,#2984,#2986,.T.); +#2984 = VERTEX_POINT('',#2985); +#2985 = CARTESIAN_POINT('',(-47.5,18.,8.)); +#2986 = LINE('',#2987,#2988); +#2987 = CARTESIAN_POINT('',(-76.25,18.,8.)); +#2988 = VECTOR('',#2989,1.); +#2989 = DIRECTION('',(1.,0.,-0.)); +#2990 = ORIENTED_EDGE('',*,*,#2991,.F.); +#2991 = EDGE_CURVE('',#2992,#2984,#2994,.T.); +#2992 = VERTEX_POINT('',#2993); +#2993 = CARTESIAN_POINT('',(-47.5,18.,40.)); +#2994 = LINE('',#2995,#2996); +#2995 = CARTESIAN_POINT('',(-47.5,18.,4.)); +#2996 = VECTOR('',#2997,1.); +#2997 = DIRECTION('',(-0.,0.,-1.)); +#2998 = ORIENTED_EDGE('',*,*,#2999,.T.); +#2999 = EDGE_CURVE('',#2992,#3000,#3002,.T.); +#3000 = VERTEX_POINT('',#3001); +#3001 = CARTESIAN_POINT('',(-41.38888888888,18.,40.)); +#3002 = LINE('',#3003,#3004); +#3003 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3004 = VECTOR('',#3005,1.); +#3005 = DIRECTION('',(1.,0.,-0.)); +#3006 = ORIENTED_EDGE('',*,*,#3007,.T.); +#3007 = EDGE_CURVE('',#3000,#3008,#3010,.T.); +#3008 = VERTEX_POINT('',#3009); +#3009 = CARTESIAN_POINT('',(-41.38888888888,18.,8.)); #3010 = LINE('',#3011,#3012); -#3011 = CARTESIAN_POINT('',(-30.27777777777,20.1,8.)); +#3011 = CARTESIAN_POINT('',(-41.38888888888,18.,4.)); #3012 = VECTOR('',#3013,1.); -#3013 = DIRECTION('',(1.,0.,-0.)); -#3014 = ORIENTED_EDGE('',*,*,#2925,.T.); -#3015 = ORIENTED_EDGE('',*,*,#3016,.T.); -#3016 = EDGE_CURVE('',#2926,#2966,#3017,.T.); -#3017 = LINE('',#3018,#3019); -#3018 = CARTESIAN_POINT('',(-30.27777777777,20.1,40.)); -#3019 = VECTOR('',#3020,1.); -#3020 = DIRECTION('',(1.,0.,-0.)); -#3021 = ORIENTED_EDGE('',*,*,#2965,.F.); -#3022 = PLANE('',#3023); -#3023 = AXIS2_PLACEMENT_3D('',#3024,#3025,#3026); -#3024 = CARTESIAN_POINT('',(-30.27777777777,20.1,8.)); -#3025 = DIRECTION('',(-0.,1.,0.)); -#3026 = DIRECTION('',(0.,0.,1.)); -#3027 = ADVANCED_FACE('',(#3028),#3034,.F.); -#3028 = FACE_BOUND('',#3029,.F.); -#3029 = EDGE_LOOP('',(#3030,#3031,#3032,#3033)); -#3030 = ORIENTED_EDGE('',*,*,#2917,.F.); -#3031 = ORIENTED_EDGE('',*,*,#2987,.T.); -#3032 = ORIENTED_EDGE('',*,*,#2957,.T.); -#3033 = ORIENTED_EDGE('',*,*,#3009,.F.); -#3034 = PLANE('',#3035); -#3035 = AXIS2_PLACEMENT_3D('',#3036,#3037,#3038); -#3036 = CARTESIAN_POINT('',(-30.27777777777,17.9,8.)); -#3037 = DIRECTION('',(0.,0.,1.)); -#3038 = DIRECTION('',(1.,0.,-0.)); -#3039 = ADVANCED_FACE('',(#3040),#3046,.T.); -#3040 = FACE_BOUND('',#3041,.T.); -#3041 = EDGE_LOOP('',(#3042,#3043,#3044,#3045)); -#3042 = ORIENTED_EDGE('',*,*,#2933,.F.); -#3043 = ORIENTED_EDGE('',*,*,#2994,.T.); -#3044 = ORIENTED_EDGE('',*,*,#2973,.T.); -#3045 = ORIENTED_EDGE('',*,*,#3016,.F.); -#3046 = PLANE('',#3047); -#3047 = AXIS2_PLACEMENT_3D('',#3048,#3049,#3050); -#3048 = CARTESIAN_POINT('',(-30.27777777777,17.9,40.)); -#3049 = DIRECTION('',(0.,0.,1.)); -#3050 = DIRECTION('',(1.,0.,-0.)); -#3051 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3055)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#3052,#3053,#3054)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#3052 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#3053 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#3054 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#3055 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3052, - 'distance_accuracy_value','confusion accuracy'); -#3056 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3057,#3059); -#3057 = ( REPRESENTATION_RELATIONSHIP('','',#2900,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3058) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#3058 = ITEM_DEFINED_TRANSFORMATION('','',#11,#79); -#3059 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #3060); -#3060 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('17','WireDuct_RightCombSlot_07', - '',#5,#2895,$); -#3061 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2897)); -#3062 = SHAPE_DEFINITION_REPRESENTATION(#3063,#3069); -#3063 = PRODUCT_DEFINITION_SHAPE('','',#3064); -#3064 = PRODUCT_DEFINITION('design','',#3065,#3068); -#3065 = PRODUCT_DEFINITION_FORMATION('','',#3066); -#3066 = PRODUCT('WireDuct_LeftCombSlot_08','WireDuct_LeftCombSlot_08','' - ,(#3067)); -#3067 = PRODUCT_CONTEXT('',#2,'mechanical'); -#3068 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#3069 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3070),#3220); -#3070 = MANIFOLD_SOLID_BREP('',#3071); -#3071 = CLOSED_SHELL('',(#3072,#3112,#3152,#3174,#3196,#3208)); -#3072 = ADVANCED_FACE('',(#3073),#3107,.F.); -#3073 = FACE_BOUND('',#3074,.F.); -#3074 = EDGE_LOOP('',(#3075,#3085,#3093,#3101)); -#3075 = ORIENTED_EDGE('',*,*,#3076,.F.); -#3076 = EDGE_CURVE('',#3077,#3079,#3081,.T.); -#3077 = VERTEX_POINT('',#3078); -#3078 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3079 = VERTEX_POINT('',#3080); -#3080 = CARTESIAN_POINT('',(-19.16666666666,-20.1,40.)); -#3081 = LINE('',#3082,#3083); -#3082 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3083 = VECTOR('',#3084,1.); -#3084 = DIRECTION('',(0.,0.,1.)); -#3085 = ORIENTED_EDGE('',*,*,#3086,.T.); -#3086 = EDGE_CURVE('',#3077,#3087,#3089,.T.); -#3087 = VERTEX_POINT('',#3088); -#3088 = CARTESIAN_POINT('',(-19.16666666666,-17.9,8.)); -#3089 = LINE('',#3090,#3091); -#3090 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3091 = VECTOR('',#3092,1.); -#3092 = DIRECTION('',(-0.,1.,0.)); -#3093 = ORIENTED_EDGE('',*,*,#3094,.T.); -#3094 = EDGE_CURVE('',#3087,#3095,#3097,.T.); -#3095 = VERTEX_POINT('',#3096); -#3096 = CARTESIAN_POINT('',(-19.16666666666,-17.9,40.)); -#3097 = LINE('',#3098,#3099); -#3098 = CARTESIAN_POINT('',(-19.16666666666,-17.9,8.)); -#3099 = VECTOR('',#3100,1.); -#3100 = DIRECTION('',(0.,0.,1.)); -#3101 = ORIENTED_EDGE('',*,*,#3102,.F.); -#3102 = EDGE_CURVE('',#3079,#3095,#3103,.T.); -#3103 = LINE('',#3104,#3105); -#3104 = CARTESIAN_POINT('',(-19.16666666666,-20.1,40.)); -#3105 = VECTOR('',#3106,1.); -#3106 = DIRECTION('',(-0.,1.,0.)); -#3107 = PLANE('',#3108); -#3108 = AXIS2_PLACEMENT_3D('',#3109,#3110,#3111); -#3109 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3110 = DIRECTION('',(1.,0.,-0.)); -#3111 = DIRECTION('',(0.,0.,1.)); -#3112 = ADVANCED_FACE('',(#3113),#3147,.T.); -#3113 = FACE_BOUND('',#3114,.T.); -#3114 = EDGE_LOOP('',(#3115,#3125,#3133,#3141)); -#3115 = ORIENTED_EDGE('',*,*,#3116,.F.); -#3116 = EDGE_CURVE('',#3117,#3119,#3121,.T.); -#3117 = VERTEX_POINT('',#3118); -#3118 = CARTESIAN_POINT('',(-14.16666666666,-20.1,8.)); -#3119 = VERTEX_POINT('',#3120); -#3120 = CARTESIAN_POINT('',(-14.16666666666,-20.1,40.)); -#3121 = LINE('',#3122,#3123); -#3122 = CARTESIAN_POINT('',(-14.16666666666,-20.1,8.)); -#3123 = VECTOR('',#3124,1.); -#3124 = DIRECTION('',(0.,0.,1.)); -#3125 = ORIENTED_EDGE('',*,*,#3126,.T.); -#3126 = EDGE_CURVE('',#3117,#3127,#3129,.T.); -#3127 = VERTEX_POINT('',#3128); -#3128 = CARTESIAN_POINT('',(-14.16666666666,-17.9,8.)); -#3129 = LINE('',#3130,#3131); -#3130 = CARTESIAN_POINT('',(-14.16666666666,-20.1,8.)); -#3131 = VECTOR('',#3132,1.); -#3132 = DIRECTION('',(-0.,1.,0.)); -#3133 = ORIENTED_EDGE('',*,*,#3134,.T.); -#3134 = EDGE_CURVE('',#3127,#3135,#3137,.T.); -#3135 = VERTEX_POINT('',#3136); -#3136 = CARTESIAN_POINT('',(-14.16666666666,-17.9,40.)); -#3137 = LINE('',#3138,#3139); -#3138 = CARTESIAN_POINT('',(-14.16666666666,-17.9,8.)); -#3139 = VECTOR('',#3140,1.); -#3140 = DIRECTION('',(0.,0.,1.)); -#3141 = ORIENTED_EDGE('',*,*,#3142,.F.); -#3142 = EDGE_CURVE('',#3119,#3135,#3143,.T.); -#3143 = LINE('',#3144,#3145); -#3144 = CARTESIAN_POINT('',(-14.16666666666,-20.1,40.)); -#3145 = VECTOR('',#3146,1.); -#3146 = DIRECTION('',(-0.,1.,0.)); -#3147 = PLANE('',#3148); -#3148 = AXIS2_PLACEMENT_3D('',#3149,#3150,#3151); -#3149 = CARTESIAN_POINT('',(-14.16666666666,-20.1,8.)); -#3150 = DIRECTION('',(1.,0.,-0.)); -#3151 = DIRECTION('',(0.,0.,1.)); -#3152 = ADVANCED_FACE('',(#3153),#3169,.F.); -#3153 = FACE_BOUND('',#3154,.F.); -#3154 = EDGE_LOOP('',(#3155,#3161,#3162,#3168)); -#3155 = ORIENTED_EDGE('',*,*,#3156,.F.); -#3156 = EDGE_CURVE('',#3077,#3117,#3157,.T.); -#3157 = LINE('',#3158,#3159); -#3158 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3159 = VECTOR('',#3160,1.); -#3160 = DIRECTION('',(1.,0.,-0.)); -#3161 = ORIENTED_EDGE('',*,*,#3076,.T.); -#3162 = ORIENTED_EDGE('',*,*,#3163,.T.); -#3163 = EDGE_CURVE('',#3079,#3119,#3164,.T.); -#3164 = LINE('',#3165,#3166); -#3165 = CARTESIAN_POINT('',(-19.16666666666,-20.1,40.)); -#3166 = VECTOR('',#3167,1.); -#3167 = DIRECTION('',(1.,0.,-0.)); -#3168 = ORIENTED_EDGE('',*,*,#3116,.F.); -#3169 = PLANE('',#3170); -#3170 = AXIS2_PLACEMENT_3D('',#3171,#3172,#3173); -#3171 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3172 = DIRECTION('',(-0.,1.,0.)); -#3173 = DIRECTION('',(0.,0.,1.)); -#3174 = ADVANCED_FACE('',(#3175),#3191,.T.); -#3175 = FACE_BOUND('',#3176,.T.); -#3176 = EDGE_LOOP('',(#3177,#3183,#3184,#3190)); -#3177 = ORIENTED_EDGE('',*,*,#3178,.F.); -#3178 = EDGE_CURVE('',#3087,#3127,#3179,.T.); -#3179 = LINE('',#3180,#3181); -#3180 = CARTESIAN_POINT('',(-19.16666666666,-17.9,8.)); -#3181 = VECTOR('',#3182,1.); -#3182 = DIRECTION('',(1.,0.,-0.)); -#3183 = ORIENTED_EDGE('',*,*,#3094,.T.); -#3184 = ORIENTED_EDGE('',*,*,#3185,.T.); -#3185 = EDGE_CURVE('',#3095,#3135,#3186,.T.); +#3013 = DIRECTION('',(-0.,0.,-1.)); +#3014 = ORIENTED_EDGE('',*,*,#3015,.T.); +#3015 = EDGE_CURVE('',#3008,#3016,#3018,.T.); +#3016 = VERTEX_POINT('',#3017); +#3017 = CARTESIAN_POINT('',(-36.38888888888,18.,8.)); +#3018 = LINE('',#3019,#3020); +#3019 = CARTESIAN_POINT('',(-70.69444444444,18.,8.)); +#3020 = VECTOR('',#3021,1.); +#3021 = DIRECTION('',(1.,0.,-0.)); +#3022 = ORIENTED_EDGE('',*,*,#3023,.F.); +#3023 = EDGE_CURVE('',#3024,#3016,#3026,.T.); +#3024 = VERTEX_POINT('',#3025); +#3025 = CARTESIAN_POINT('',(-36.38888888888,18.,40.)); +#3026 = LINE('',#3027,#3028); +#3027 = CARTESIAN_POINT('',(-36.38888888888,18.,4.)); +#3028 = VECTOR('',#3029,1.); +#3029 = DIRECTION('',(-0.,0.,-1.)); +#3030 = ORIENTED_EDGE('',*,*,#3031,.T.); +#3031 = EDGE_CURVE('',#3024,#3032,#3034,.T.); +#3032 = VERTEX_POINT('',#3033); +#3033 = CARTESIAN_POINT('',(-30.27777777777,18.,40.)); +#3034 = LINE('',#3035,#3036); +#3035 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3036 = VECTOR('',#3037,1.); +#3037 = DIRECTION('',(1.,0.,-0.)); +#3038 = ORIENTED_EDGE('',*,*,#3039,.T.); +#3039 = EDGE_CURVE('',#3032,#3040,#3042,.T.); +#3040 = VERTEX_POINT('',#3041); +#3041 = CARTESIAN_POINT('',(-30.27777777777,18.,8.)); +#3042 = LINE('',#3043,#3044); +#3043 = CARTESIAN_POINT('',(-30.27777777777,18.,4.)); +#3044 = VECTOR('',#3045,1.); +#3045 = DIRECTION('',(-0.,0.,-1.)); +#3046 = ORIENTED_EDGE('',*,*,#3047,.T.); +#3047 = EDGE_CURVE('',#3040,#3048,#3050,.T.); +#3048 = VERTEX_POINT('',#3049); +#3049 = CARTESIAN_POINT('',(-25.27777777777,18.,8.)); +#3050 = LINE('',#3051,#3052); +#3051 = CARTESIAN_POINT('',(-65.13888888888,18.,8.)); +#3052 = VECTOR('',#3053,1.); +#3053 = DIRECTION('',(1.,0.,-0.)); +#3054 = ORIENTED_EDGE('',*,*,#3055,.F.); +#3055 = EDGE_CURVE('',#3056,#3048,#3058,.T.); +#3056 = VERTEX_POINT('',#3057); +#3057 = CARTESIAN_POINT('',(-25.27777777777,18.,40.)); +#3058 = LINE('',#3059,#3060); +#3059 = CARTESIAN_POINT('',(-25.27777777777,18.,4.)); +#3060 = VECTOR('',#3061,1.); +#3061 = DIRECTION('',(-0.,0.,-1.)); +#3062 = ORIENTED_EDGE('',*,*,#3063,.T.); +#3063 = EDGE_CURVE('',#3056,#3064,#3066,.T.); +#3064 = VERTEX_POINT('',#3065); +#3065 = CARTESIAN_POINT('',(-19.16666666666,18.,40.)); +#3066 = LINE('',#3067,#3068); +#3067 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3068 = VECTOR('',#3069,1.); +#3069 = DIRECTION('',(1.,0.,-0.)); +#3070 = ORIENTED_EDGE('',*,*,#3071,.T.); +#3071 = EDGE_CURVE('',#3064,#3072,#3074,.T.); +#3072 = VERTEX_POINT('',#3073); +#3073 = CARTESIAN_POINT('',(-19.16666666666,18.,8.)); +#3074 = LINE('',#3075,#3076); +#3075 = CARTESIAN_POINT('',(-19.16666666666,18.,4.)); +#3076 = VECTOR('',#3077,1.); +#3077 = DIRECTION('',(-0.,0.,-1.)); +#3078 = ORIENTED_EDGE('',*,*,#3079,.T.); +#3079 = EDGE_CURVE('',#3072,#3080,#3082,.T.); +#3080 = VERTEX_POINT('',#3081); +#3081 = CARTESIAN_POINT('',(-14.16666666666,18.,8.)); +#3082 = LINE('',#3083,#3084); +#3083 = CARTESIAN_POINT('',(-59.58333333333,18.,8.)); +#3084 = VECTOR('',#3085,1.); +#3085 = DIRECTION('',(1.,0.,-0.)); +#3086 = ORIENTED_EDGE('',*,*,#3087,.F.); +#3087 = EDGE_CURVE('',#3088,#3080,#3090,.T.); +#3088 = VERTEX_POINT('',#3089); +#3089 = CARTESIAN_POINT('',(-14.16666666666,18.,40.)); +#3090 = LINE('',#3091,#3092); +#3091 = CARTESIAN_POINT('',(-14.16666666666,18.,4.)); +#3092 = VECTOR('',#3093,1.); +#3093 = DIRECTION('',(-0.,0.,-1.)); +#3094 = ORIENTED_EDGE('',*,*,#3095,.T.); +#3095 = EDGE_CURVE('',#3088,#3096,#3098,.T.); +#3096 = VERTEX_POINT('',#3097); +#3097 = CARTESIAN_POINT('',(-8.055555555556,18.,40.)); +#3098 = LINE('',#3099,#3100); +#3099 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3100 = VECTOR('',#3101,1.); +#3101 = DIRECTION('',(1.,0.,-0.)); +#3102 = ORIENTED_EDGE('',*,*,#3103,.T.); +#3103 = EDGE_CURVE('',#3096,#3104,#3106,.T.); +#3104 = VERTEX_POINT('',#3105); +#3105 = CARTESIAN_POINT('',(-8.055555555556,18.,8.)); +#3106 = LINE('',#3107,#3108); +#3107 = CARTESIAN_POINT('',(-8.055555555556,18.,4.)); +#3108 = VECTOR('',#3109,1.); +#3109 = DIRECTION('',(-0.,0.,-1.)); +#3110 = ORIENTED_EDGE('',*,*,#3111,.T.); +#3111 = EDGE_CURVE('',#3104,#3112,#3114,.T.); +#3112 = VERTEX_POINT('',#3113); +#3113 = CARTESIAN_POINT('',(-3.055555555556,18.,8.)); +#3114 = LINE('',#3115,#3116); +#3115 = CARTESIAN_POINT('',(-54.02777777777,18.,8.)); +#3116 = VECTOR('',#3117,1.); +#3117 = DIRECTION('',(1.,0.,-0.)); +#3118 = ORIENTED_EDGE('',*,*,#3119,.F.); +#3119 = EDGE_CURVE('',#3120,#3112,#3122,.T.); +#3120 = VERTEX_POINT('',#3121); +#3121 = CARTESIAN_POINT('',(-3.055555555556,18.,40.)); +#3122 = LINE('',#3123,#3124); +#3123 = CARTESIAN_POINT('',(-3.055555555556,18.,4.)); +#3124 = VECTOR('',#3125,1.); +#3125 = DIRECTION('',(-0.,0.,-1.)); +#3126 = ORIENTED_EDGE('',*,*,#3127,.T.); +#3127 = EDGE_CURVE('',#3120,#3128,#3130,.T.); +#3128 = VERTEX_POINT('',#3129); +#3129 = CARTESIAN_POINT('',(3.055555555556,18.,40.)); +#3130 = LINE('',#3131,#3132); +#3131 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3132 = VECTOR('',#3133,1.); +#3133 = DIRECTION('',(1.,0.,-0.)); +#3134 = ORIENTED_EDGE('',*,*,#3135,.T.); +#3135 = EDGE_CURVE('',#3128,#3136,#3138,.T.); +#3136 = VERTEX_POINT('',#3137); +#3137 = CARTESIAN_POINT('',(3.055555555556,18.,8.)); +#3138 = LINE('',#3139,#3140); +#3139 = CARTESIAN_POINT('',(3.055555555556,18.,4.)); +#3140 = VECTOR('',#3141,1.); +#3141 = DIRECTION('',(-0.,0.,-1.)); +#3142 = ORIENTED_EDGE('',*,*,#3143,.T.); +#3143 = EDGE_CURVE('',#3136,#3144,#3146,.T.); +#3144 = VERTEX_POINT('',#3145); +#3145 = CARTESIAN_POINT('',(8.055555555556,18.,8.)); +#3146 = LINE('',#3147,#3148); +#3147 = CARTESIAN_POINT('',(-48.47222222222,18.,8.)); +#3148 = VECTOR('',#3149,1.); +#3149 = DIRECTION('',(1.,0.,-0.)); +#3150 = ORIENTED_EDGE('',*,*,#3151,.F.); +#3151 = EDGE_CURVE('',#3152,#3144,#3154,.T.); +#3152 = VERTEX_POINT('',#3153); +#3153 = CARTESIAN_POINT('',(8.055555555556,18.,40.)); +#3154 = LINE('',#3155,#3156); +#3155 = CARTESIAN_POINT('',(8.055555555556,18.,4.)); +#3156 = VECTOR('',#3157,1.); +#3157 = DIRECTION('',(-0.,0.,-1.)); +#3158 = ORIENTED_EDGE('',*,*,#3159,.T.); +#3159 = EDGE_CURVE('',#3152,#3160,#3162,.T.); +#3160 = VERTEX_POINT('',#3161); +#3161 = CARTESIAN_POINT('',(14.166666666667,18.,40.)); +#3162 = LINE('',#3163,#3164); +#3163 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3164 = VECTOR('',#3165,1.); +#3165 = DIRECTION('',(1.,0.,-0.)); +#3166 = ORIENTED_EDGE('',*,*,#3167,.T.); +#3167 = EDGE_CURVE('',#3160,#3168,#3170,.T.); +#3168 = VERTEX_POINT('',#3169); +#3169 = CARTESIAN_POINT('',(14.166666666667,18.,8.)); +#3170 = LINE('',#3171,#3172); +#3171 = CARTESIAN_POINT('',(14.166666666667,18.,4.)); +#3172 = VECTOR('',#3173,1.); +#3173 = DIRECTION('',(-0.,0.,-1.)); +#3174 = ORIENTED_EDGE('',*,*,#3175,.T.); +#3175 = EDGE_CURVE('',#3168,#3176,#3178,.T.); +#3176 = VERTEX_POINT('',#3177); +#3177 = CARTESIAN_POINT('',(19.166666666667,18.,8.)); +#3178 = LINE('',#3179,#3180); +#3179 = CARTESIAN_POINT('',(-42.91666666666,18.,8.)); +#3180 = VECTOR('',#3181,1.); +#3181 = DIRECTION('',(1.,0.,-0.)); +#3182 = ORIENTED_EDGE('',*,*,#3183,.F.); +#3183 = EDGE_CURVE('',#3184,#3176,#3186,.T.); +#3184 = VERTEX_POINT('',#3185); +#3185 = CARTESIAN_POINT('',(19.166666666667,18.,40.)); #3186 = LINE('',#3187,#3188); -#3187 = CARTESIAN_POINT('',(-19.16666666666,-17.9,40.)); +#3187 = CARTESIAN_POINT('',(19.166666666667,18.,4.)); #3188 = VECTOR('',#3189,1.); -#3189 = DIRECTION('',(1.,0.,-0.)); -#3190 = ORIENTED_EDGE('',*,*,#3134,.F.); -#3191 = PLANE('',#3192); -#3192 = AXIS2_PLACEMENT_3D('',#3193,#3194,#3195); -#3193 = CARTESIAN_POINT('',(-19.16666666666,-17.9,8.)); -#3194 = DIRECTION('',(-0.,1.,0.)); -#3195 = DIRECTION('',(0.,0.,1.)); -#3196 = ADVANCED_FACE('',(#3197),#3203,.F.); -#3197 = FACE_BOUND('',#3198,.F.); -#3198 = EDGE_LOOP('',(#3199,#3200,#3201,#3202)); -#3199 = ORIENTED_EDGE('',*,*,#3086,.F.); -#3200 = ORIENTED_EDGE('',*,*,#3156,.T.); -#3201 = ORIENTED_EDGE('',*,*,#3126,.T.); -#3202 = ORIENTED_EDGE('',*,*,#3178,.F.); -#3203 = PLANE('',#3204); -#3204 = AXIS2_PLACEMENT_3D('',#3205,#3206,#3207); -#3205 = CARTESIAN_POINT('',(-19.16666666666,-20.1,8.)); -#3206 = DIRECTION('',(0.,0.,1.)); -#3207 = DIRECTION('',(1.,0.,-0.)); -#3208 = ADVANCED_FACE('',(#3209),#3215,.T.); -#3209 = FACE_BOUND('',#3210,.T.); -#3210 = EDGE_LOOP('',(#3211,#3212,#3213,#3214)); -#3211 = ORIENTED_EDGE('',*,*,#3102,.F.); -#3212 = ORIENTED_EDGE('',*,*,#3163,.T.); -#3213 = ORIENTED_EDGE('',*,*,#3142,.T.); -#3214 = ORIENTED_EDGE('',*,*,#3185,.F.); -#3215 = PLANE('',#3216); -#3216 = AXIS2_PLACEMENT_3D('',#3217,#3218,#3219); -#3217 = CARTESIAN_POINT('',(-19.16666666666,-20.1,40.)); -#3218 = DIRECTION('',(0.,0.,1.)); -#3219 = DIRECTION('',(1.,0.,-0.)); -#3220 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3224)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#3221,#3222,#3223)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#3221 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#3222 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#3223 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#3224 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3221, - 'distance_accuracy_value','confusion accuracy'); -#3225 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3226,#3228); -#3226 = ( REPRESENTATION_RELATIONSHIP('','',#3069,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3227) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#3227 = ITEM_DEFINED_TRANSFORMATION('','',#11,#83); -#3228 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #3229); -#3229 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('18','WireDuct_LeftCombSlot_08', - '',#5,#3064,$); -#3230 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3066)); -#3231 = SHAPE_DEFINITION_REPRESENTATION(#3232,#3238); -#3232 = PRODUCT_DEFINITION_SHAPE('','',#3233); -#3233 = PRODUCT_DEFINITION('design','',#3234,#3237); -#3234 = PRODUCT_DEFINITION_FORMATION('','',#3235); -#3235 = PRODUCT('WireDuct_RightCombSlot_08','WireDuct_RightCombSlot_08', - '',(#3236)); -#3236 = PRODUCT_CONTEXT('',#2,'mechanical'); -#3237 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#3238 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3239),#3389); -#3239 = MANIFOLD_SOLID_BREP('',#3240); -#3240 = CLOSED_SHELL('',(#3241,#3281,#3321,#3343,#3365,#3377)); -#3241 = ADVANCED_FACE('',(#3242),#3276,.F.); -#3242 = FACE_BOUND('',#3243,.F.); -#3243 = EDGE_LOOP('',(#3244,#3254,#3262,#3270)); -#3244 = ORIENTED_EDGE('',*,*,#3245,.F.); -#3245 = EDGE_CURVE('',#3246,#3248,#3250,.T.); -#3246 = VERTEX_POINT('',#3247); -#3247 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); +#3189 = DIRECTION('',(-0.,0.,-1.)); +#3190 = ORIENTED_EDGE('',*,*,#3191,.T.); +#3191 = EDGE_CURVE('',#3184,#3192,#3194,.T.); +#3192 = VERTEX_POINT('',#3193); +#3193 = CARTESIAN_POINT('',(25.277777777778,18.,40.)); +#3194 = LINE('',#3195,#3196); +#3195 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3196 = VECTOR('',#3197,1.); +#3197 = DIRECTION('',(1.,0.,-0.)); +#3198 = ORIENTED_EDGE('',*,*,#3199,.T.); +#3199 = EDGE_CURVE('',#3192,#3200,#3202,.T.); +#3200 = VERTEX_POINT('',#3201); +#3201 = CARTESIAN_POINT('',(25.277777777778,18.,8.)); +#3202 = LINE('',#3203,#3204); +#3203 = CARTESIAN_POINT('',(25.277777777778,18.,4.)); +#3204 = VECTOR('',#3205,1.); +#3205 = DIRECTION('',(-0.,0.,-1.)); +#3206 = ORIENTED_EDGE('',*,*,#3207,.T.); +#3207 = EDGE_CURVE('',#3200,#3208,#3210,.T.); +#3208 = VERTEX_POINT('',#3209); +#3209 = CARTESIAN_POINT('',(30.277777777778,18.,8.)); +#3210 = LINE('',#3211,#3212); +#3211 = CARTESIAN_POINT('',(-37.36111111111,18.,8.)); +#3212 = VECTOR('',#3213,1.); +#3213 = DIRECTION('',(1.,0.,-0.)); +#3214 = ORIENTED_EDGE('',*,*,#3215,.F.); +#3215 = EDGE_CURVE('',#3216,#3208,#3218,.T.); +#3216 = VERTEX_POINT('',#3217); +#3217 = CARTESIAN_POINT('',(30.277777777778,18.,40.)); +#3218 = LINE('',#3219,#3220); +#3219 = CARTESIAN_POINT('',(30.277777777778,18.,4.)); +#3220 = VECTOR('',#3221,1.); +#3221 = DIRECTION('',(-0.,0.,-1.)); +#3222 = ORIENTED_EDGE('',*,*,#3223,.T.); +#3223 = EDGE_CURVE('',#3216,#3224,#3226,.T.); +#3224 = VERTEX_POINT('',#3225); +#3225 = CARTESIAN_POINT('',(36.388888888889,18.,40.)); +#3226 = LINE('',#3227,#3228); +#3227 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3228 = VECTOR('',#3229,1.); +#3229 = DIRECTION('',(1.,0.,-0.)); +#3230 = ORIENTED_EDGE('',*,*,#3231,.T.); +#3231 = EDGE_CURVE('',#3224,#3232,#3234,.T.); +#3232 = VERTEX_POINT('',#3233); +#3233 = CARTESIAN_POINT('',(36.388888888889,18.,8.)); +#3234 = LINE('',#3235,#3236); +#3235 = CARTESIAN_POINT('',(36.388888888889,18.,4.)); +#3236 = VECTOR('',#3237,1.); +#3237 = DIRECTION('',(-0.,0.,-1.)); +#3238 = ORIENTED_EDGE('',*,*,#3239,.T.); +#3239 = EDGE_CURVE('',#3232,#3240,#3242,.T.); +#3240 = VERTEX_POINT('',#3241); +#3241 = CARTESIAN_POINT('',(41.388888888889,18.,8.)); +#3242 = LINE('',#3243,#3244); +#3243 = CARTESIAN_POINT('',(-31.80555555555,18.,8.)); +#3244 = VECTOR('',#3245,1.); +#3245 = DIRECTION('',(1.,0.,-0.)); +#3246 = ORIENTED_EDGE('',*,*,#3247,.F.); +#3247 = EDGE_CURVE('',#3248,#3240,#3250,.T.); #3248 = VERTEX_POINT('',#3249); -#3249 = CARTESIAN_POINT('',(-19.16666666666,17.9,40.)); +#3249 = CARTESIAN_POINT('',(41.388888888889,18.,40.)); #3250 = LINE('',#3251,#3252); -#3251 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); +#3251 = CARTESIAN_POINT('',(41.388888888889,18.,4.)); #3252 = VECTOR('',#3253,1.); -#3253 = DIRECTION('',(0.,0.,1.)); +#3253 = DIRECTION('',(-0.,0.,-1.)); #3254 = ORIENTED_EDGE('',*,*,#3255,.T.); -#3255 = EDGE_CURVE('',#3246,#3256,#3258,.T.); +#3255 = EDGE_CURVE('',#3248,#3256,#3258,.T.); #3256 = VERTEX_POINT('',#3257); -#3257 = CARTESIAN_POINT('',(-19.16666666666,20.1,8.)); +#3257 = CARTESIAN_POINT('',(47.5,18.,40.)); #3258 = LINE('',#3259,#3260); -#3259 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); +#3259 = CARTESIAN_POINT('',(-100.,18.,40.)); #3260 = VECTOR('',#3261,1.); -#3261 = DIRECTION('',(-0.,1.,0.)); +#3261 = DIRECTION('',(1.,0.,-0.)); #3262 = ORIENTED_EDGE('',*,*,#3263,.T.); #3263 = EDGE_CURVE('',#3256,#3264,#3266,.T.); #3264 = VERTEX_POINT('',#3265); -#3265 = CARTESIAN_POINT('',(-19.16666666666,20.1,40.)); +#3265 = CARTESIAN_POINT('',(47.5,18.,8.)); #3266 = LINE('',#3267,#3268); -#3267 = CARTESIAN_POINT('',(-19.16666666666,20.1,8.)); +#3267 = CARTESIAN_POINT('',(47.5,18.,4.)); #3268 = VECTOR('',#3269,1.); -#3269 = DIRECTION('',(0.,0.,1.)); -#3270 = ORIENTED_EDGE('',*,*,#3271,.F.); -#3271 = EDGE_CURVE('',#3248,#3264,#3272,.T.); -#3272 = LINE('',#3273,#3274); -#3273 = CARTESIAN_POINT('',(-19.16666666666,17.9,40.)); -#3274 = VECTOR('',#3275,1.); -#3275 = DIRECTION('',(-0.,1.,0.)); -#3276 = PLANE('',#3277); -#3277 = AXIS2_PLACEMENT_3D('',#3278,#3279,#3280); -#3278 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); -#3279 = DIRECTION('',(1.,0.,-0.)); -#3280 = DIRECTION('',(0.,0.,1.)); -#3281 = ADVANCED_FACE('',(#3282),#3316,.T.); -#3282 = FACE_BOUND('',#3283,.T.); -#3283 = EDGE_LOOP('',(#3284,#3294,#3302,#3310)); -#3284 = ORIENTED_EDGE('',*,*,#3285,.F.); -#3285 = EDGE_CURVE('',#3286,#3288,#3290,.T.); -#3286 = VERTEX_POINT('',#3287); -#3287 = CARTESIAN_POINT('',(-14.16666666666,17.9,8.)); +#3269 = DIRECTION('',(-0.,0.,-1.)); +#3270 = ORIENTED_EDGE('',*,*,#3271,.T.); +#3271 = EDGE_CURVE('',#3264,#3272,#3274,.T.); +#3272 = VERTEX_POINT('',#3273); +#3273 = CARTESIAN_POINT('',(52.5,18.,8.)); +#3274 = LINE('',#3275,#3276); +#3275 = CARTESIAN_POINT('',(-26.25,18.,8.)); +#3276 = VECTOR('',#3277,1.); +#3277 = DIRECTION('',(1.,0.,-0.)); +#3278 = ORIENTED_EDGE('',*,*,#3279,.F.); +#3279 = EDGE_CURVE('',#3280,#3272,#3282,.T.); +#3280 = VERTEX_POINT('',#3281); +#3281 = CARTESIAN_POINT('',(52.5,18.,40.)); +#3282 = LINE('',#3283,#3284); +#3283 = CARTESIAN_POINT('',(52.5,18.,4.)); +#3284 = VECTOR('',#3285,1.); +#3285 = DIRECTION('',(-0.,0.,-1.)); +#3286 = ORIENTED_EDGE('',*,*,#3287,.T.); +#3287 = EDGE_CURVE('',#3280,#3288,#3290,.T.); #3288 = VERTEX_POINT('',#3289); -#3289 = CARTESIAN_POINT('',(-14.16666666666,17.9,40.)); +#3289 = CARTESIAN_POINT('',(58.611111111111,18.,40.)); #3290 = LINE('',#3291,#3292); -#3291 = CARTESIAN_POINT('',(-14.16666666666,17.9,8.)); +#3291 = CARTESIAN_POINT('',(-100.,18.,40.)); #3292 = VECTOR('',#3293,1.); -#3293 = DIRECTION('',(0.,0.,1.)); +#3293 = DIRECTION('',(1.,0.,-0.)); #3294 = ORIENTED_EDGE('',*,*,#3295,.T.); -#3295 = EDGE_CURVE('',#3286,#3296,#3298,.T.); +#3295 = EDGE_CURVE('',#3288,#3296,#3298,.T.); #3296 = VERTEX_POINT('',#3297); -#3297 = CARTESIAN_POINT('',(-14.16666666666,20.1,8.)); +#3297 = CARTESIAN_POINT('',(58.611111111111,18.,8.)); #3298 = LINE('',#3299,#3300); -#3299 = CARTESIAN_POINT('',(-14.16666666666,17.9,8.)); +#3299 = CARTESIAN_POINT('',(58.611111111111,18.,4.)); #3300 = VECTOR('',#3301,1.); -#3301 = DIRECTION('',(-0.,1.,0.)); +#3301 = DIRECTION('',(-0.,0.,-1.)); #3302 = ORIENTED_EDGE('',*,*,#3303,.T.); #3303 = EDGE_CURVE('',#3296,#3304,#3306,.T.); #3304 = VERTEX_POINT('',#3305); -#3305 = CARTESIAN_POINT('',(-14.16666666666,20.1,40.)); +#3305 = CARTESIAN_POINT('',(63.611111111111,18.,8.)); #3306 = LINE('',#3307,#3308); -#3307 = CARTESIAN_POINT('',(-14.16666666666,20.1,8.)); +#3307 = CARTESIAN_POINT('',(-20.69444444444,18.,8.)); #3308 = VECTOR('',#3309,1.); -#3309 = DIRECTION('',(0.,0.,1.)); +#3309 = DIRECTION('',(1.,0.,-0.)); #3310 = ORIENTED_EDGE('',*,*,#3311,.F.); -#3311 = EDGE_CURVE('',#3288,#3304,#3312,.T.); -#3312 = LINE('',#3313,#3314); -#3313 = CARTESIAN_POINT('',(-14.16666666666,17.9,40.)); -#3314 = VECTOR('',#3315,1.); -#3315 = DIRECTION('',(-0.,1.,0.)); -#3316 = PLANE('',#3317); -#3317 = AXIS2_PLACEMENT_3D('',#3318,#3319,#3320); -#3318 = CARTESIAN_POINT('',(-14.16666666666,17.9,8.)); -#3319 = DIRECTION('',(1.,0.,-0.)); -#3320 = DIRECTION('',(0.,0.,1.)); -#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('',#3246,#3286,#3326,.T.); -#3326 = LINE('',#3327,#3328); -#3327 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); -#3328 = VECTOR('',#3329,1.); -#3329 = DIRECTION('',(1.,0.,-0.)); -#3330 = ORIENTED_EDGE('',*,*,#3245,.T.); -#3331 = ORIENTED_EDGE('',*,*,#3332,.T.); -#3332 = EDGE_CURVE('',#3248,#3288,#3333,.T.); -#3333 = LINE('',#3334,#3335); -#3334 = CARTESIAN_POINT('',(-19.16666666666,17.9,40.)); -#3335 = VECTOR('',#3336,1.); -#3336 = DIRECTION('',(1.,0.,-0.)); -#3337 = ORIENTED_EDGE('',*,*,#3285,.F.); -#3338 = PLANE('',#3339); -#3339 = AXIS2_PLACEMENT_3D('',#3340,#3341,#3342); -#3340 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); -#3341 = DIRECTION('',(-0.,1.,0.)); -#3342 = DIRECTION('',(0.,0.,1.)); -#3343 = ADVANCED_FACE('',(#3344),#3360,.T.); -#3344 = FACE_BOUND('',#3345,.T.); -#3345 = EDGE_LOOP('',(#3346,#3352,#3353,#3359)); -#3346 = ORIENTED_EDGE('',*,*,#3347,.F.); -#3347 = EDGE_CURVE('',#3256,#3296,#3348,.T.); -#3348 = LINE('',#3349,#3350); -#3349 = CARTESIAN_POINT('',(-19.16666666666,20.1,8.)); -#3350 = VECTOR('',#3351,1.); -#3351 = DIRECTION('',(1.,0.,-0.)); -#3352 = ORIENTED_EDGE('',*,*,#3263,.T.); -#3353 = ORIENTED_EDGE('',*,*,#3354,.T.); -#3354 = EDGE_CURVE('',#3264,#3304,#3355,.T.); -#3355 = LINE('',#3356,#3357); -#3356 = CARTESIAN_POINT('',(-19.16666666666,20.1,40.)); -#3357 = VECTOR('',#3358,1.); -#3358 = DIRECTION('',(1.,0.,-0.)); -#3359 = ORIENTED_EDGE('',*,*,#3303,.F.); -#3360 = PLANE('',#3361); -#3361 = AXIS2_PLACEMENT_3D('',#3362,#3363,#3364); -#3362 = CARTESIAN_POINT('',(-19.16666666666,20.1,8.)); -#3363 = DIRECTION('',(-0.,1.,0.)); -#3364 = DIRECTION('',(0.,0.,1.)); -#3365 = ADVANCED_FACE('',(#3366),#3372,.F.); -#3366 = FACE_BOUND('',#3367,.F.); -#3367 = EDGE_LOOP('',(#3368,#3369,#3370,#3371)); -#3368 = ORIENTED_EDGE('',*,*,#3255,.F.); -#3369 = ORIENTED_EDGE('',*,*,#3325,.T.); -#3370 = ORIENTED_EDGE('',*,*,#3295,.T.); -#3371 = ORIENTED_EDGE('',*,*,#3347,.F.); -#3372 = PLANE('',#3373); -#3373 = AXIS2_PLACEMENT_3D('',#3374,#3375,#3376); -#3374 = CARTESIAN_POINT('',(-19.16666666666,17.9,8.)); -#3375 = DIRECTION('',(0.,0.,1.)); -#3376 = DIRECTION('',(1.,0.,-0.)); -#3377 = ADVANCED_FACE('',(#3378),#3384,.T.); -#3378 = FACE_BOUND('',#3379,.T.); -#3379 = EDGE_LOOP('',(#3380,#3381,#3382,#3383)); -#3380 = ORIENTED_EDGE('',*,*,#3271,.F.); -#3381 = ORIENTED_EDGE('',*,*,#3332,.T.); -#3382 = ORIENTED_EDGE('',*,*,#3311,.T.); -#3383 = ORIENTED_EDGE('',*,*,#3354,.F.); -#3384 = PLANE('',#3385); -#3385 = AXIS2_PLACEMENT_3D('',#3386,#3387,#3388); -#3386 = CARTESIAN_POINT('',(-19.16666666666,17.9,40.)); -#3387 = DIRECTION('',(0.,0.,1.)); -#3388 = DIRECTION('',(1.,0.,-0.)); -#3389 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3393)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#3390,#3391,#3392)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#3390 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#3391 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#3392 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#3393 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3390, - 'distance_accuracy_value','confusion accuracy'); -#3394 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3395,#3397); -#3395 = ( REPRESENTATION_RELATIONSHIP('','',#3238,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3396) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#3396 = ITEM_DEFINED_TRANSFORMATION('','',#11,#87); -#3397 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #3398); -#3398 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('19','WireDuct_RightCombSlot_08', - '',#5,#3233,$); -#3399 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3235)); -#3400 = SHAPE_DEFINITION_REPRESENTATION(#3401,#3407); -#3401 = PRODUCT_DEFINITION_SHAPE('','',#3402); -#3402 = PRODUCT_DEFINITION('design','',#3403,#3406); -#3403 = PRODUCT_DEFINITION_FORMATION('','',#3404); -#3404 = PRODUCT('WireDuct_LeftCombSlot_09','WireDuct_LeftCombSlot_09','' - ,(#3405)); -#3405 = PRODUCT_CONTEXT('',#2,'mechanical'); -#3406 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#3407 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3408),#3558); -#3408 = MANIFOLD_SOLID_BREP('',#3409); -#3409 = CLOSED_SHELL('',(#3410,#3450,#3490,#3512,#3534,#3546)); -#3410 = ADVANCED_FACE('',(#3411),#3445,.F.); -#3411 = FACE_BOUND('',#3412,.F.); -#3412 = EDGE_LOOP('',(#3413,#3423,#3431,#3439)); -#3413 = ORIENTED_EDGE('',*,*,#3414,.F.); -#3414 = EDGE_CURVE('',#3415,#3417,#3419,.T.); -#3415 = VERTEX_POINT('',#3416); -#3416 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3417 = VERTEX_POINT('',#3418); -#3418 = CARTESIAN_POINT('',(-8.055555555556,-20.1,40.)); -#3419 = LINE('',#3420,#3421); -#3420 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3421 = VECTOR('',#3422,1.); -#3422 = DIRECTION('',(0.,0.,1.)); -#3423 = ORIENTED_EDGE('',*,*,#3424,.T.); -#3424 = EDGE_CURVE('',#3415,#3425,#3427,.T.); -#3425 = VERTEX_POINT('',#3426); -#3426 = CARTESIAN_POINT('',(-8.055555555556,-17.9,8.)); -#3427 = LINE('',#3428,#3429); -#3428 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3429 = VECTOR('',#3430,1.); -#3430 = DIRECTION('',(-0.,1.,0.)); -#3431 = ORIENTED_EDGE('',*,*,#3432,.T.); -#3432 = EDGE_CURVE('',#3425,#3433,#3435,.T.); -#3433 = VERTEX_POINT('',#3434); -#3434 = CARTESIAN_POINT('',(-8.055555555556,-17.9,40.)); -#3435 = LINE('',#3436,#3437); -#3436 = CARTESIAN_POINT('',(-8.055555555556,-17.9,8.)); -#3437 = VECTOR('',#3438,1.); -#3438 = DIRECTION('',(0.,0.,1.)); -#3439 = ORIENTED_EDGE('',*,*,#3440,.F.); -#3440 = EDGE_CURVE('',#3417,#3433,#3441,.T.); -#3441 = LINE('',#3442,#3443); -#3442 = CARTESIAN_POINT('',(-8.055555555556,-20.1,40.)); -#3443 = VECTOR('',#3444,1.); -#3444 = DIRECTION('',(-0.,1.,0.)); -#3445 = PLANE('',#3446); -#3446 = AXIS2_PLACEMENT_3D('',#3447,#3448,#3449); -#3447 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3448 = DIRECTION('',(1.,0.,-0.)); -#3449 = DIRECTION('',(0.,0.,1.)); -#3450 = ADVANCED_FACE('',(#3451),#3485,.T.); -#3451 = FACE_BOUND('',#3452,.T.); -#3452 = EDGE_LOOP('',(#3453,#3463,#3471,#3479)); -#3453 = ORIENTED_EDGE('',*,*,#3454,.F.); -#3454 = EDGE_CURVE('',#3455,#3457,#3459,.T.); -#3455 = VERTEX_POINT('',#3456); -#3456 = CARTESIAN_POINT('',(-3.055555555556,-20.1,8.)); -#3457 = VERTEX_POINT('',#3458); -#3458 = CARTESIAN_POINT('',(-3.055555555556,-20.1,40.)); -#3459 = LINE('',#3460,#3461); -#3460 = CARTESIAN_POINT('',(-3.055555555556,-20.1,8.)); -#3461 = VECTOR('',#3462,1.); -#3462 = DIRECTION('',(0.,0.,1.)); -#3463 = ORIENTED_EDGE('',*,*,#3464,.T.); -#3464 = EDGE_CURVE('',#3455,#3465,#3467,.T.); -#3465 = VERTEX_POINT('',#3466); -#3466 = CARTESIAN_POINT('',(-3.055555555556,-17.9,8.)); -#3467 = LINE('',#3468,#3469); -#3468 = CARTESIAN_POINT('',(-3.055555555556,-20.1,8.)); -#3469 = VECTOR('',#3470,1.); -#3470 = DIRECTION('',(-0.,1.,0.)); -#3471 = ORIENTED_EDGE('',*,*,#3472,.T.); -#3472 = EDGE_CURVE('',#3465,#3473,#3475,.T.); -#3473 = VERTEX_POINT('',#3474); -#3474 = CARTESIAN_POINT('',(-3.055555555556,-17.9,40.)); -#3475 = LINE('',#3476,#3477); -#3476 = CARTESIAN_POINT('',(-3.055555555556,-17.9,8.)); -#3477 = VECTOR('',#3478,1.); -#3478 = DIRECTION('',(0.,0.,1.)); -#3479 = ORIENTED_EDGE('',*,*,#3480,.F.); -#3480 = EDGE_CURVE('',#3457,#3473,#3481,.T.); -#3481 = LINE('',#3482,#3483); -#3482 = CARTESIAN_POINT('',(-3.055555555556,-20.1,40.)); -#3483 = VECTOR('',#3484,1.); -#3484 = DIRECTION('',(-0.,1.,0.)); -#3485 = PLANE('',#3486); -#3486 = AXIS2_PLACEMENT_3D('',#3487,#3488,#3489); -#3487 = CARTESIAN_POINT('',(-3.055555555556,-20.1,8.)); -#3488 = DIRECTION('',(1.,0.,-0.)); -#3489 = DIRECTION('',(0.,0.,1.)); -#3490 = ADVANCED_FACE('',(#3491),#3507,.F.); -#3491 = FACE_BOUND('',#3492,.F.); -#3492 = EDGE_LOOP('',(#3493,#3499,#3500,#3506)); -#3493 = ORIENTED_EDGE('',*,*,#3494,.F.); -#3494 = EDGE_CURVE('',#3415,#3455,#3495,.T.); -#3495 = LINE('',#3496,#3497); -#3496 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3497 = VECTOR('',#3498,1.); +#3311 = EDGE_CURVE('',#3312,#3304,#3314,.T.); +#3312 = VERTEX_POINT('',#3313); +#3313 = CARTESIAN_POINT('',(63.611111111111,18.,40.)); +#3314 = LINE('',#3315,#3316); +#3315 = CARTESIAN_POINT('',(63.611111111111,18.,4.)); +#3316 = VECTOR('',#3317,1.); +#3317 = DIRECTION('',(-0.,0.,-1.)); +#3318 = ORIENTED_EDGE('',*,*,#3319,.T.); +#3319 = EDGE_CURVE('',#3312,#3320,#3322,.T.); +#3320 = VERTEX_POINT('',#3321); +#3321 = CARTESIAN_POINT('',(69.722222222222,18.,40.)); +#3322 = LINE('',#3323,#3324); +#3323 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3324 = VECTOR('',#3325,1.); +#3325 = DIRECTION('',(1.,0.,-0.)); +#3326 = ORIENTED_EDGE('',*,*,#3327,.T.); +#3327 = EDGE_CURVE('',#3320,#3328,#3330,.T.); +#3328 = VERTEX_POINT('',#3329); +#3329 = CARTESIAN_POINT('',(69.722222222222,18.,8.)); +#3330 = LINE('',#3331,#3332); +#3331 = CARTESIAN_POINT('',(69.722222222222,18.,4.)); +#3332 = VECTOR('',#3333,1.); +#3333 = DIRECTION('',(-0.,0.,-1.)); +#3334 = ORIENTED_EDGE('',*,*,#3335,.T.); +#3335 = EDGE_CURVE('',#3328,#3336,#3338,.T.); +#3336 = VERTEX_POINT('',#3337); +#3337 = CARTESIAN_POINT('',(74.722222222222,18.,8.)); +#3338 = LINE('',#3339,#3340); +#3339 = CARTESIAN_POINT('',(-15.13888888888,18.,8.)); +#3340 = VECTOR('',#3341,1.); +#3341 = DIRECTION('',(1.,0.,-0.)); +#3342 = ORIENTED_EDGE('',*,*,#3343,.F.); +#3343 = EDGE_CURVE('',#3344,#3336,#3346,.T.); +#3344 = VERTEX_POINT('',#3345); +#3345 = CARTESIAN_POINT('',(74.722222222222,18.,40.)); +#3346 = LINE('',#3347,#3348); +#3347 = CARTESIAN_POINT('',(74.722222222222,18.,4.)); +#3348 = VECTOR('',#3349,1.); +#3349 = DIRECTION('',(-0.,0.,-1.)); +#3350 = ORIENTED_EDGE('',*,*,#3351,.T.); +#3351 = EDGE_CURVE('',#3344,#3352,#3354,.T.); +#3352 = VERTEX_POINT('',#3353); +#3353 = CARTESIAN_POINT('',(80.833333333333,18.,40.)); +#3354 = LINE('',#3355,#3356); +#3355 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3356 = VECTOR('',#3357,1.); +#3357 = DIRECTION('',(1.,0.,-0.)); +#3358 = ORIENTED_EDGE('',*,*,#3359,.T.); +#3359 = EDGE_CURVE('',#3352,#3360,#3362,.T.); +#3360 = VERTEX_POINT('',#3361); +#3361 = CARTESIAN_POINT('',(80.833333333333,18.,8.)); +#3362 = LINE('',#3363,#3364); +#3363 = CARTESIAN_POINT('',(80.833333333333,18.,4.)); +#3364 = VECTOR('',#3365,1.); +#3365 = DIRECTION('',(-0.,0.,-1.)); +#3366 = ORIENTED_EDGE('',*,*,#3367,.T.); +#3367 = EDGE_CURVE('',#3360,#3368,#3370,.T.); +#3368 = VERTEX_POINT('',#3369); +#3369 = CARTESIAN_POINT('',(85.833333333333,18.,8.)); +#3370 = LINE('',#3371,#3372); +#3371 = CARTESIAN_POINT('',(-9.583333333333,18.,8.)); +#3372 = VECTOR('',#3373,1.); +#3373 = DIRECTION('',(1.,0.,-0.)); +#3374 = ORIENTED_EDGE('',*,*,#3375,.F.); +#3375 = EDGE_CURVE('',#3376,#3368,#3378,.T.); +#3376 = VERTEX_POINT('',#3377); +#3377 = CARTESIAN_POINT('',(85.833333333333,18.,40.)); +#3378 = LINE('',#3379,#3380); +#3379 = CARTESIAN_POINT('',(85.833333333333,18.,4.)); +#3380 = VECTOR('',#3381,1.); +#3381 = DIRECTION('',(-0.,0.,-1.)); +#3382 = ORIENTED_EDGE('',*,*,#3383,.T.); +#3383 = EDGE_CURVE('',#3376,#3384,#3386,.T.); +#3384 = VERTEX_POINT('',#3385); +#3385 = CARTESIAN_POINT('',(91.944444444444,18.,40.)); +#3386 = LINE('',#3387,#3388); +#3387 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3388 = VECTOR('',#3389,1.); +#3389 = DIRECTION('',(1.,0.,-0.)); +#3390 = ORIENTED_EDGE('',*,*,#3391,.T.); +#3391 = EDGE_CURVE('',#3384,#3392,#3394,.T.); +#3392 = VERTEX_POINT('',#3393); +#3393 = CARTESIAN_POINT('',(91.944444444444,18.,8.)); +#3394 = LINE('',#3395,#3396); +#3395 = CARTESIAN_POINT('',(91.944444444444,18.,4.)); +#3396 = VECTOR('',#3397,1.); +#3397 = DIRECTION('',(-0.,0.,-1.)); +#3398 = ORIENTED_EDGE('',*,*,#3399,.T.); +#3399 = EDGE_CURVE('',#3392,#3400,#3402,.T.); +#3400 = VERTEX_POINT('',#3401); +#3401 = CARTESIAN_POINT('',(96.944444444444,18.,8.)); +#3402 = LINE('',#3403,#3404); +#3403 = CARTESIAN_POINT('',(-4.027777777778,18.,8.)); +#3404 = VECTOR('',#3405,1.); +#3405 = DIRECTION('',(1.,0.,-0.)); +#3406 = ORIENTED_EDGE('',*,*,#3407,.F.); +#3407 = EDGE_CURVE('',#3408,#3400,#3410,.T.); +#3408 = VERTEX_POINT('',#3409); +#3409 = CARTESIAN_POINT('',(96.944444444444,18.,40.)); +#3410 = LINE('',#3411,#3412); +#3411 = CARTESIAN_POINT('',(96.944444444444,18.,4.)); +#3412 = VECTOR('',#3413,1.); +#3413 = DIRECTION('',(-0.,0.,-1.)); +#3414 = ORIENTED_EDGE('',*,*,#3415,.T.); +#3415 = EDGE_CURVE('',#3408,#2823,#3416,.T.); +#3416 = LINE('',#3417,#3418); +#3417 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3418 = VECTOR('',#3419,1.); +#3419 = DIRECTION('',(1.,0.,-0.)); +#3420 = PLANE('',#3421); +#3421 = AXIS2_PLACEMENT_3D('',#3422,#3423,#3424); +#3422 = CARTESIAN_POINT('',(-100.,18.,0.)); +#3423 = DIRECTION('',(-0.,1.,0.)); +#3424 = DIRECTION('',(0.,0.,1.)); +#3425 = ADVANCED_FACE('',(#3426),#3437,.F.); +#3426 = FACE_BOUND('',#3427,.F.); +#3427 = EDGE_LOOP('',(#3428,#3429,#3435,#3436)); +#3428 = ORIENTED_EDGE('',*,*,#1442,.F.); +#3429 = ORIENTED_EDGE('',*,*,#3430,.F.); +#3430 = EDGE_CURVE('',#1531,#1443,#3431,.T.); +#3431 = LINE('',#3432,#3433); +#3432 = CARTESIAN_POINT('',(-57.8,-5.388445916248E-16,-0.2)); +#3433 = VECTOR('',#3434,1.); +#3434 = DIRECTION('',(0.,0.,1.)); +#3435 = ORIENTED_EDGE('',*,*,#1530,.T.); +#3436 = ORIENTED_EDGE('',*,*,#3430,.T.); +#3437 = CYLINDRICAL_SURFACE('',#3438,2.2); +#3438 = AXIS2_PLACEMENT_3D('',#3439,#3440,#3441); +#3439 = CARTESIAN_POINT('',(-60.,0.,-0.2)); +#3440 = DIRECTION('',(0.,0.,1.)); +#3441 = DIRECTION('',(1.,0.,-0.)); +#3442 = ADVANCED_FACE('',(#3443),#3454,.F.); +#3443 = FACE_BOUND('',#3444,.F.); +#3444 = EDGE_LOOP('',(#3445,#3446,#3452,#3453)); +#3445 = ORIENTED_EDGE('',*,*,#1453,.F.); +#3446 = ORIENTED_EDGE('',*,*,#3447,.F.); +#3447 = EDGE_CURVE('',#1542,#1454,#3448,.T.); +#3448 = LINE('',#3449,#3450); +#3449 = CARTESIAN_POINT('',(2.2,-5.388445916248E-16,-0.2)); +#3450 = VECTOR('',#3451,1.); +#3451 = DIRECTION('',(0.,0.,1.)); +#3452 = ORIENTED_EDGE('',*,*,#1541,.T.); +#3453 = ORIENTED_EDGE('',*,*,#3447,.T.); +#3454 = CYLINDRICAL_SURFACE('',#3455,2.2); +#3455 = AXIS2_PLACEMENT_3D('',#3456,#3457,#3458); +#3456 = CARTESIAN_POINT('',(0.,0.,-0.2)); +#3457 = DIRECTION('',(0.,0.,1.)); +#3458 = DIRECTION('',(1.,0.,-0.)); +#3459 = ADVANCED_FACE('',(#3460),#3471,.F.); +#3460 = FACE_BOUND('',#3461,.F.); +#3461 = EDGE_LOOP('',(#3462,#3463,#3469,#3470)); +#3462 = ORIENTED_EDGE('',*,*,#1464,.F.); +#3463 = ORIENTED_EDGE('',*,*,#3464,.F.); +#3464 = EDGE_CURVE('',#1553,#1465,#3465,.T.); +#3465 = LINE('',#3466,#3467); +#3466 = CARTESIAN_POINT('',(62.2,-5.388445916248E-16,-0.2)); +#3467 = VECTOR('',#3468,1.); +#3468 = DIRECTION('',(0.,0.,1.)); +#3469 = ORIENTED_EDGE('',*,*,#1552,.T.); +#3470 = ORIENTED_EDGE('',*,*,#3464,.T.); +#3471 = CYLINDRICAL_SURFACE('',#3472,2.2); +#3472 = AXIS2_PLACEMENT_3D('',#3473,#3474,#3475); +#3473 = CARTESIAN_POINT('',(60.,0.,-0.2)); +#3474 = DIRECTION('',(0.,0.,1.)); +#3475 = DIRECTION('',(1.,0.,-0.)); +#3476 = ADVANCED_FACE('',(#3477),#3495,.F.); +#3477 = FACE_BOUND('',#3478,.F.); +#3478 = EDGE_LOOP('',(#3479,#3480,#3481,#3489)); +#3479 = ORIENTED_EDGE('',*,*,#2831,.F.); +#3480 = ORIENTED_EDGE('',*,*,#1498,.T.); +#3481 = ORIENTED_EDGE('',*,*,#3482,.T.); +#3482 = EDGE_CURVE('',#1491,#3483,#3485,.T.); +#3483 = VERTEX_POINT('',#3484); +#3484 = CARTESIAN_POINT('',(-100.,20.,40.)); +#3485 = LINE('',#3486,#3487); +#3486 = CARTESIAN_POINT('',(-100.,20.,0.)); +#3487 = VECTOR('',#3488,1.); +#3488 = DIRECTION('',(0.,0.,1.)); +#3489 = ORIENTED_EDGE('',*,*,#3490,.F.); +#3490 = EDGE_CURVE('',#2832,#3483,#3491,.T.); +#3491 = LINE('',#3492,#3493); +#3492 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3493 = VECTOR('',#3494,1.); +#3494 = DIRECTION('',(-0.,1.,0.)); +#3495 = PLANE('',#3496); +#3496 = AXIS2_PLACEMENT_3D('',#3497,#3498,#3499); +#3497 = CARTESIAN_POINT('',(-100.,18.,0.)); #3498 = DIRECTION('',(1.,0.,-0.)); -#3499 = ORIENTED_EDGE('',*,*,#3414,.T.); -#3500 = ORIENTED_EDGE('',*,*,#3501,.T.); -#3501 = EDGE_CURVE('',#3417,#3457,#3502,.T.); -#3502 = LINE('',#3503,#3504); -#3503 = CARTESIAN_POINT('',(-8.055555555556,-20.1,40.)); -#3504 = VECTOR('',#3505,1.); -#3505 = DIRECTION('',(1.,0.,-0.)); -#3506 = ORIENTED_EDGE('',*,*,#3454,.F.); -#3507 = PLANE('',#3508); -#3508 = AXIS2_PLACEMENT_3D('',#3509,#3510,#3511); -#3509 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3510 = DIRECTION('',(-0.,1.,0.)); -#3511 = DIRECTION('',(0.,0.,1.)); -#3512 = ADVANCED_FACE('',(#3513),#3529,.T.); -#3513 = FACE_BOUND('',#3514,.T.); -#3514 = EDGE_LOOP('',(#3515,#3521,#3522,#3528)); -#3515 = ORIENTED_EDGE('',*,*,#3516,.F.); -#3516 = EDGE_CURVE('',#3425,#3465,#3517,.T.); -#3517 = LINE('',#3518,#3519); -#3518 = CARTESIAN_POINT('',(-8.055555555556,-17.9,8.)); -#3519 = VECTOR('',#3520,1.); -#3520 = DIRECTION('',(1.,0.,-0.)); -#3521 = ORIENTED_EDGE('',*,*,#3432,.T.); -#3522 = ORIENTED_EDGE('',*,*,#3523,.T.); -#3523 = EDGE_CURVE('',#3433,#3473,#3524,.T.); -#3524 = LINE('',#3525,#3526); -#3525 = CARTESIAN_POINT('',(-8.055555555556,-17.9,40.)); -#3526 = VECTOR('',#3527,1.); -#3527 = DIRECTION('',(1.,0.,-0.)); -#3528 = ORIENTED_EDGE('',*,*,#3472,.F.); -#3529 = PLANE('',#3530); -#3530 = AXIS2_PLACEMENT_3D('',#3531,#3532,#3533); -#3531 = CARTESIAN_POINT('',(-8.055555555556,-17.9,8.)); -#3532 = DIRECTION('',(-0.,1.,0.)); -#3533 = DIRECTION('',(0.,0.,1.)); -#3534 = ADVANCED_FACE('',(#3535),#3541,.F.); -#3535 = FACE_BOUND('',#3536,.F.); -#3536 = EDGE_LOOP('',(#3537,#3538,#3539,#3540)); -#3537 = ORIENTED_EDGE('',*,*,#3424,.F.); -#3538 = ORIENTED_EDGE('',*,*,#3494,.T.); -#3539 = ORIENTED_EDGE('',*,*,#3464,.T.); -#3540 = ORIENTED_EDGE('',*,*,#3516,.F.); -#3541 = PLANE('',#3542); -#3542 = AXIS2_PLACEMENT_3D('',#3543,#3544,#3545); -#3543 = CARTESIAN_POINT('',(-8.055555555556,-20.1,8.)); -#3544 = DIRECTION('',(0.,0.,1.)); -#3545 = DIRECTION('',(1.,0.,-0.)); -#3546 = ADVANCED_FACE('',(#3547),#3553,.T.); -#3547 = FACE_BOUND('',#3548,.T.); -#3548 = EDGE_LOOP('',(#3549,#3550,#3551,#3552)); -#3549 = ORIENTED_EDGE('',*,*,#3440,.F.); -#3550 = ORIENTED_EDGE('',*,*,#3501,.T.); -#3551 = ORIENTED_EDGE('',*,*,#3480,.T.); -#3552 = ORIENTED_EDGE('',*,*,#3523,.F.); -#3553 = PLANE('',#3554); -#3554 = AXIS2_PLACEMENT_3D('',#3555,#3556,#3557); -#3555 = CARTESIAN_POINT('',(-8.055555555556,-20.1,40.)); -#3556 = DIRECTION('',(0.,0.,1.)); -#3557 = DIRECTION('',(1.,0.,-0.)); -#3558 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3562)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#3559,#3560,#3561)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#3559 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#3560 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#3561 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#3562 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3559, - 'distance_accuracy_value','confusion accuracy'); -#3563 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3564,#3566); -#3564 = ( REPRESENTATION_RELATIONSHIP('','',#3407,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3565) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#3565 = ITEM_DEFINED_TRANSFORMATION('','',#11,#91); -#3566 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #3567); -#3567 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('20','WireDuct_LeftCombSlot_09', - '',#5,#3402,$); -#3568 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3404)); -#3569 = SHAPE_DEFINITION_REPRESENTATION(#3570,#3576); -#3570 = PRODUCT_DEFINITION_SHAPE('','',#3571); -#3571 = PRODUCT_DEFINITION('design','',#3572,#3575); -#3572 = PRODUCT_DEFINITION_FORMATION('','',#3573); -#3573 = PRODUCT('WireDuct_RightCombSlot_09','WireDuct_RightCombSlot_09', - '',(#3574)); -#3574 = PRODUCT_CONTEXT('',#2,'mechanical'); -#3575 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#3576 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3577),#3727); -#3577 = MANIFOLD_SOLID_BREP('',#3578); -#3578 = CLOSED_SHELL('',(#3579,#3619,#3659,#3681,#3703,#3715)); -#3579 = ADVANCED_FACE('',(#3580),#3614,.F.); -#3580 = FACE_BOUND('',#3581,.F.); -#3581 = EDGE_LOOP('',(#3582,#3592,#3600,#3608)); -#3582 = ORIENTED_EDGE('',*,*,#3583,.F.); -#3583 = EDGE_CURVE('',#3584,#3586,#3588,.T.); -#3584 = VERTEX_POINT('',#3585); -#3585 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); -#3586 = VERTEX_POINT('',#3587); -#3587 = CARTESIAN_POINT('',(-8.055555555556,17.9,40.)); -#3588 = LINE('',#3589,#3590); -#3589 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); -#3590 = VECTOR('',#3591,1.); -#3591 = DIRECTION('',(0.,0.,1.)); -#3592 = ORIENTED_EDGE('',*,*,#3593,.T.); -#3593 = EDGE_CURVE('',#3584,#3594,#3596,.T.); +#3499 = DIRECTION('',(0.,0.,1.)); +#3500 = ADVANCED_FACE('',(#3501),#3526,.T.); +#3501 = FACE_BOUND('',#3502,.T.); +#3502 = EDGE_LOOP('',(#3503,#3511,#3512,#3520)); +#3503 = ORIENTED_EDGE('',*,*,#3504,.F.); +#3504 = EDGE_CURVE('',#1483,#3505,#3507,.T.); +#3505 = VERTEX_POINT('',#3506); +#3506 = CARTESIAN_POINT('',(100.,20.,0.)); +#3507 = LINE('',#3508,#3509); +#3508 = CARTESIAN_POINT('',(-100.,20.,0.)); +#3509 = VECTOR('',#3510,1.); +#3510 = DIRECTION('',(1.,0.,-0.)); +#3511 = ORIENTED_EDGE('',*,*,#1490,.T.); +#3512 = ORIENTED_EDGE('',*,*,#3513,.T.); +#3513 = EDGE_CURVE('',#1491,#3514,#3516,.T.); +#3514 = VERTEX_POINT('',#3515); +#3515 = CARTESIAN_POINT('',(100.,20.,2.)); +#3516 = LINE('',#3517,#3518); +#3517 = CARTESIAN_POINT('',(-100.,20.,2.)); +#3518 = VECTOR('',#3519,1.); +#3519 = DIRECTION('',(1.,0.,-0.)); +#3520 = ORIENTED_EDGE('',*,*,#3521,.F.); +#3521 = EDGE_CURVE('',#3505,#3514,#3522,.T.); +#3522 = LINE('',#3523,#3524); +#3523 = CARTESIAN_POINT('',(100.,20.,0.)); +#3524 = VECTOR('',#3525,1.); +#3525 = DIRECTION('',(0.,0.,1.)); +#3526 = PLANE('',#3527); +#3527 = AXIS2_PLACEMENT_3D('',#3528,#3529,#3530); +#3528 = CARTESIAN_POINT('',(-100.,20.,0.)); +#3529 = DIRECTION('',(-0.,1.,0.)); +#3530 = DIRECTION('',(0.,0.,1.)); +#3531 = ADVANCED_FACE('',(#3532),#3543,.F.); +#3532 = FACE_BOUND('',#3533,.F.); +#3533 = EDGE_LOOP('',(#3534,#3535,#3536,#3542)); +#3534 = ORIENTED_EDGE('',*,*,#1482,.F.); +#3535 = ORIENTED_EDGE('',*,*,#1522,.T.); +#3536 = ORIENTED_EDGE('',*,*,#3537,.T.); +#3537 = EDGE_CURVE('',#1515,#3505,#3538,.T.); +#3538 = LINE('',#3539,#3540); +#3539 = CARTESIAN_POINT('',(100.,-20.,0.)); +#3540 = VECTOR('',#3541,1.); +#3541 = DIRECTION('',(-0.,1.,0.)); +#3542 = ORIENTED_EDGE('',*,*,#3504,.F.); +#3543 = PLANE('',#3544); +#3544 = AXIS2_PLACEMENT_3D('',#3545,#3546,#3547); +#3545 = CARTESIAN_POINT('',(-100.,-20.,0.)); +#3546 = DIRECTION('',(0.,0.,1.)); +#3547 = DIRECTION('',(1.,0.,-0.)); +#3548 = ADVANCED_FACE('',(#3549),#3560,.T.); +#3549 = FACE_BOUND('',#3550,.T.); +#3550 = EDGE_LOOP('',(#3551,#3552,#3553,#3554)); +#3551 = ORIENTED_EDGE('',*,*,#1588,.F.); +#3552 = ORIENTED_EDGE('',*,*,#3537,.T.); +#3553 = ORIENTED_EDGE('',*,*,#3521,.T.); +#3554 = ORIENTED_EDGE('',*,*,#3555,.F.); +#3555 = EDGE_CURVE('',#1427,#3514,#3556,.T.); +#3556 = LINE('',#3557,#3558); +#3557 = CARTESIAN_POINT('',(100.,-20.,2.)); +#3558 = VECTOR('',#3559,1.); +#3559 = DIRECTION('',(-0.,1.,0.)); +#3560 = PLANE('',#3561); +#3561 = AXIS2_PLACEMENT_3D('',#3562,#3563,#3564); +#3562 = CARTESIAN_POINT('',(100.,-20.,0.)); +#3563 = DIRECTION('',(1.,0.,-0.)); +#3564 = DIRECTION('',(0.,0.,1.)); +#3565 = ADVANCED_FACE('',(#3566),#3584,.T.); +#3566 = FACE_BOUND('',#3567,.T.); +#3567 = EDGE_LOOP('',(#3568,#3569,#3570,#3578)); +#3568 = ORIENTED_EDGE('',*,*,#2822,.F.); +#3569 = ORIENTED_EDGE('',*,*,#3555,.T.); +#3570 = ORIENTED_EDGE('',*,*,#3571,.T.); +#3571 = EDGE_CURVE('',#3514,#3572,#3574,.T.); +#3572 = VERTEX_POINT('',#3573); +#3573 = CARTESIAN_POINT('',(100.,20.,40.)); +#3574 = LINE('',#3575,#3576); +#3575 = CARTESIAN_POINT('',(100.,20.,0.)); +#3576 = VECTOR('',#3577,1.); +#3577 = DIRECTION('',(0.,0.,1.)); +#3578 = ORIENTED_EDGE('',*,*,#3579,.F.); +#3579 = EDGE_CURVE('',#2823,#3572,#3580,.T.); +#3580 = LINE('',#3581,#3582); +#3581 = CARTESIAN_POINT('',(100.,18.,40.)); +#3582 = VECTOR('',#3583,1.); +#3583 = DIRECTION('',(-0.,1.,0.)); +#3584 = PLANE('',#3585); +#3585 = AXIS2_PLACEMENT_3D('',#3586,#3587,#3588); +#3586 = CARTESIAN_POINT('',(100.,18.,0.)); +#3587 = DIRECTION('',(1.,0.,-0.)); +#3588 = DIRECTION('',(0.,0.,1.)); +#3589 = ADVANCED_FACE('',(#3590),#3608,.T.); +#3590 = FACE_BOUND('',#3591,.T.); +#3591 = EDGE_LOOP('',(#3592,#3600,#3606,#3607)); +#3592 = ORIENTED_EDGE('',*,*,#3593,.F.); +#3593 = EDGE_CURVE('',#3594,#3572,#3596,.T.); #3594 = VERTEX_POINT('',#3595); -#3595 = CARTESIAN_POINT('',(-8.055555555556,20.1,8.)); +#3595 = CARTESIAN_POINT('',(96.944444444444,20.,40.)); #3596 = LINE('',#3597,#3598); -#3597 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); +#3597 = CARTESIAN_POINT('',(-100.,20.,40.)); #3598 = VECTOR('',#3599,1.); -#3599 = DIRECTION('',(-0.,1.,0.)); -#3600 = ORIENTED_EDGE('',*,*,#3601,.T.); -#3601 = EDGE_CURVE('',#3594,#3602,#3604,.T.); -#3602 = VERTEX_POINT('',#3603); -#3603 = CARTESIAN_POINT('',(-8.055555555556,20.1,40.)); -#3604 = LINE('',#3605,#3606); -#3605 = CARTESIAN_POINT('',(-8.055555555556,20.1,8.)); -#3606 = VECTOR('',#3607,1.); -#3607 = DIRECTION('',(0.,0.,1.)); -#3608 = ORIENTED_EDGE('',*,*,#3609,.F.); -#3609 = EDGE_CURVE('',#3586,#3602,#3610,.T.); -#3610 = LINE('',#3611,#3612); -#3611 = CARTESIAN_POINT('',(-8.055555555556,17.9,40.)); -#3612 = VECTOR('',#3613,1.); -#3613 = DIRECTION('',(-0.,1.,0.)); -#3614 = PLANE('',#3615); -#3615 = AXIS2_PLACEMENT_3D('',#3616,#3617,#3618); -#3616 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); -#3617 = DIRECTION('',(1.,0.,-0.)); -#3618 = DIRECTION('',(0.,0.,1.)); -#3619 = ADVANCED_FACE('',(#3620),#3654,.T.); -#3620 = FACE_BOUND('',#3621,.T.); -#3621 = EDGE_LOOP('',(#3622,#3632,#3640,#3648)); -#3622 = ORIENTED_EDGE('',*,*,#3623,.F.); -#3623 = EDGE_CURVE('',#3624,#3626,#3628,.T.); -#3624 = VERTEX_POINT('',#3625); -#3625 = CARTESIAN_POINT('',(-3.055555555556,17.9,8.)); -#3626 = VERTEX_POINT('',#3627); -#3627 = CARTESIAN_POINT('',(-3.055555555556,17.9,40.)); +#3599 = DIRECTION('',(1.,0.,-0.)); +#3600 = ORIENTED_EDGE('',*,*,#3601,.F.); +#3601 = EDGE_CURVE('',#3408,#3594,#3602,.T.); +#3602 = LINE('',#3603,#3604); +#3603 = CARTESIAN_POINT('',(96.944444444444,17.8,40.)); +#3604 = VECTOR('',#3605,1.); +#3605 = DIRECTION('',(-0.,1.,0.)); +#3606 = ORIENTED_EDGE('',*,*,#3415,.T.); +#3607 = ORIENTED_EDGE('',*,*,#3579,.T.); +#3608 = PLANE('',#3609); +#3609 = AXIS2_PLACEMENT_3D('',#3610,#3611,#3612); +#3610 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3611 = DIRECTION('',(0.,0.,1.)); +#3612 = DIRECTION('',(1.,0.,-0.)); +#3613 = ADVANCED_FACE('',(#3614),#3632,.F.); +#3614 = FACE_BOUND('',#3615,.F.); +#3615 = EDGE_LOOP('',(#3616,#3617,#3618,#3626)); +#3616 = ORIENTED_EDGE('',*,*,#3601,.F.); +#3617 = ORIENTED_EDGE('',*,*,#3407,.T.); +#3618 = ORIENTED_EDGE('',*,*,#3619,.T.); +#3619 = EDGE_CURVE('',#3400,#3620,#3622,.T.); +#3620 = VERTEX_POINT('',#3621); +#3621 = CARTESIAN_POINT('',(96.944444444444,20.,8.)); +#3622 = LINE('',#3623,#3624); +#3623 = CARTESIAN_POINT('',(96.944444444444,17.8,8.)); +#3624 = VECTOR('',#3625,1.); +#3625 = DIRECTION('',(-0.,1.,0.)); +#3626 = ORIENTED_EDGE('',*,*,#3627,.F.); +#3627 = EDGE_CURVE('',#3594,#3620,#3628,.T.); #3628 = LINE('',#3629,#3630); -#3629 = CARTESIAN_POINT('',(-3.055555555556,17.9,8.)); +#3629 = CARTESIAN_POINT('',(96.944444444444,20.,4.)); #3630 = VECTOR('',#3631,1.); -#3631 = DIRECTION('',(0.,0.,1.)); -#3632 = ORIENTED_EDGE('',*,*,#3633,.T.); -#3633 = EDGE_CURVE('',#3624,#3634,#3636,.T.); -#3634 = VERTEX_POINT('',#3635); -#3635 = CARTESIAN_POINT('',(-3.055555555556,20.1,8.)); -#3636 = LINE('',#3637,#3638); -#3637 = CARTESIAN_POINT('',(-3.055555555556,17.9,8.)); -#3638 = VECTOR('',#3639,1.); -#3639 = DIRECTION('',(-0.,1.,0.)); -#3640 = ORIENTED_EDGE('',*,*,#3641,.T.); -#3641 = EDGE_CURVE('',#3634,#3642,#3644,.T.); +#3631 = DIRECTION('',(-0.,0.,-1.)); +#3632 = PLANE('',#3633); +#3633 = AXIS2_PLACEMENT_3D('',#3634,#3635,#3636); +#3634 = CARTESIAN_POINT('',(96.944444444444,17.8,8.)); +#3635 = DIRECTION('',(1.,0.,-0.)); +#3636 = DIRECTION('',(0.,0.,1.)); +#3637 = ADVANCED_FACE('',(#3638),#3656,.T.); +#3638 = FACE_BOUND('',#3639,.T.); +#3639 = EDGE_LOOP('',(#3640,#3648,#3649,#3650)); +#3640 = ORIENTED_EDGE('',*,*,#3641,.F.); +#3641 = EDGE_CURVE('',#3392,#3642,#3644,.T.); #3642 = VERTEX_POINT('',#3643); -#3643 = CARTESIAN_POINT('',(-3.055555555556,20.1,40.)); +#3643 = CARTESIAN_POINT('',(91.944444444444,20.,8.)); #3644 = LINE('',#3645,#3646); -#3645 = CARTESIAN_POINT('',(-3.055555555556,20.1,8.)); +#3645 = CARTESIAN_POINT('',(91.944444444444,17.8,8.)); #3646 = VECTOR('',#3647,1.); -#3647 = DIRECTION('',(0.,0.,1.)); -#3648 = ORIENTED_EDGE('',*,*,#3649,.F.); -#3649 = EDGE_CURVE('',#3626,#3642,#3650,.T.); -#3650 = LINE('',#3651,#3652); -#3651 = CARTESIAN_POINT('',(-3.055555555556,17.9,40.)); -#3652 = VECTOR('',#3653,1.); -#3653 = DIRECTION('',(-0.,1.,0.)); -#3654 = PLANE('',#3655); -#3655 = AXIS2_PLACEMENT_3D('',#3656,#3657,#3658); -#3656 = CARTESIAN_POINT('',(-3.055555555556,17.9,8.)); -#3657 = DIRECTION('',(1.,0.,-0.)); -#3658 = DIRECTION('',(0.,0.,1.)); -#3659 = ADVANCED_FACE('',(#3660),#3676,.F.); -#3660 = FACE_BOUND('',#3661,.F.); -#3661 = EDGE_LOOP('',(#3662,#3668,#3669,#3675)); -#3662 = ORIENTED_EDGE('',*,*,#3663,.F.); -#3663 = EDGE_CURVE('',#3584,#3624,#3664,.T.); -#3664 = LINE('',#3665,#3666); -#3665 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); -#3666 = VECTOR('',#3667,1.); -#3667 = DIRECTION('',(1.,0.,-0.)); -#3668 = ORIENTED_EDGE('',*,*,#3583,.T.); -#3669 = ORIENTED_EDGE('',*,*,#3670,.T.); -#3670 = EDGE_CURVE('',#3586,#3626,#3671,.T.); -#3671 = LINE('',#3672,#3673); -#3672 = CARTESIAN_POINT('',(-8.055555555556,17.9,40.)); -#3673 = VECTOR('',#3674,1.); -#3674 = DIRECTION('',(1.,0.,-0.)); -#3675 = ORIENTED_EDGE('',*,*,#3623,.F.); -#3676 = PLANE('',#3677); -#3677 = AXIS2_PLACEMENT_3D('',#3678,#3679,#3680); -#3678 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); -#3679 = DIRECTION('',(-0.,1.,0.)); -#3680 = DIRECTION('',(0.,0.,1.)); -#3681 = ADVANCED_FACE('',(#3682),#3698,.T.); -#3682 = FACE_BOUND('',#3683,.T.); -#3683 = EDGE_LOOP('',(#3684,#3690,#3691,#3697)); -#3684 = ORIENTED_EDGE('',*,*,#3685,.F.); -#3685 = EDGE_CURVE('',#3594,#3634,#3686,.T.); -#3686 = LINE('',#3687,#3688); -#3687 = CARTESIAN_POINT('',(-8.055555555556,20.1,8.)); -#3688 = VECTOR('',#3689,1.); -#3689 = DIRECTION('',(1.,0.,-0.)); -#3690 = ORIENTED_EDGE('',*,*,#3601,.T.); -#3691 = ORIENTED_EDGE('',*,*,#3692,.T.); -#3692 = EDGE_CURVE('',#3602,#3642,#3693,.T.); -#3693 = LINE('',#3694,#3695); -#3694 = CARTESIAN_POINT('',(-8.055555555556,20.1,40.)); -#3695 = VECTOR('',#3696,1.); -#3696 = DIRECTION('',(1.,0.,-0.)); -#3697 = ORIENTED_EDGE('',*,*,#3641,.F.); -#3698 = PLANE('',#3699); -#3699 = AXIS2_PLACEMENT_3D('',#3700,#3701,#3702); -#3700 = CARTESIAN_POINT('',(-8.055555555556,20.1,8.)); +#3647 = DIRECTION('',(-0.,1.,0.)); +#3648 = ORIENTED_EDGE('',*,*,#3399,.T.); +#3649 = ORIENTED_EDGE('',*,*,#3619,.T.); +#3650 = ORIENTED_EDGE('',*,*,#3651,.F.); +#3651 = EDGE_CURVE('',#3642,#3620,#3652,.T.); +#3652 = LINE('',#3653,#3654); +#3653 = CARTESIAN_POINT('',(-4.027777777778,20.,8.)); +#3654 = VECTOR('',#3655,1.); +#3655 = DIRECTION('',(1.,0.,-0.)); +#3656 = PLANE('',#3657); +#3657 = AXIS2_PLACEMENT_3D('',#3658,#3659,#3660); +#3658 = CARTESIAN_POINT('',(91.944444444444,17.8,8.)); +#3659 = DIRECTION('',(0.,0.,1.)); +#3660 = DIRECTION('',(1.,0.,-0.)); +#3661 = ADVANCED_FACE('',(#3662),#3680,.T.); +#3662 = FACE_BOUND('',#3663,.T.); +#3663 = EDGE_LOOP('',(#3664,#3672,#3673,#3674)); +#3664 = ORIENTED_EDGE('',*,*,#3665,.F.); +#3665 = EDGE_CURVE('',#3384,#3666,#3668,.T.); +#3666 = VERTEX_POINT('',#3667); +#3667 = CARTESIAN_POINT('',(91.944444444444,20.,40.)); +#3668 = LINE('',#3669,#3670); +#3669 = CARTESIAN_POINT('',(91.944444444444,17.8,40.)); +#3670 = VECTOR('',#3671,1.); +#3671 = DIRECTION('',(-0.,1.,0.)); +#3672 = ORIENTED_EDGE('',*,*,#3391,.T.); +#3673 = ORIENTED_EDGE('',*,*,#3641,.T.); +#3674 = ORIENTED_EDGE('',*,*,#3675,.F.); +#3675 = EDGE_CURVE('',#3666,#3642,#3676,.T.); +#3676 = LINE('',#3677,#3678); +#3677 = CARTESIAN_POINT('',(91.944444444444,20.,4.)); +#3678 = VECTOR('',#3679,1.); +#3679 = DIRECTION('',(-0.,0.,-1.)); +#3680 = PLANE('',#3681); +#3681 = AXIS2_PLACEMENT_3D('',#3682,#3683,#3684); +#3682 = CARTESIAN_POINT('',(91.944444444444,17.8,8.)); +#3683 = DIRECTION('',(1.,0.,-0.)); +#3684 = DIRECTION('',(0.,0.,1.)); +#3685 = ADVANCED_FACE('',(#3686),#3704,.T.); +#3686 = FACE_BOUND('',#3687,.T.); +#3687 = EDGE_LOOP('',(#3688,#3696,#3702,#3703)); +#3688 = ORIENTED_EDGE('',*,*,#3689,.F.); +#3689 = EDGE_CURVE('',#3690,#3666,#3692,.T.); +#3690 = VERTEX_POINT('',#3691); +#3691 = CARTESIAN_POINT('',(85.833333333333,20.,40.)); +#3692 = LINE('',#3693,#3694); +#3693 = CARTESIAN_POINT('',(-100.,20.,40.)); +#3694 = VECTOR('',#3695,1.); +#3695 = DIRECTION('',(1.,0.,-0.)); +#3696 = ORIENTED_EDGE('',*,*,#3697,.F.); +#3697 = EDGE_CURVE('',#3376,#3690,#3698,.T.); +#3698 = LINE('',#3699,#3700); +#3699 = CARTESIAN_POINT('',(85.833333333333,17.8,40.)); +#3700 = VECTOR('',#3701,1.); #3701 = DIRECTION('',(-0.,1.,0.)); -#3702 = DIRECTION('',(0.,0.,1.)); -#3703 = ADVANCED_FACE('',(#3704),#3710,.F.); -#3704 = FACE_BOUND('',#3705,.F.); -#3705 = EDGE_LOOP('',(#3706,#3707,#3708,#3709)); -#3706 = ORIENTED_EDGE('',*,*,#3593,.F.); -#3707 = ORIENTED_EDGE('',*,*,#3663,.T.); -#3708 = ORIENTED_EDGE('',*,*,#3633,.T.); -#3709 = ORIENTED_EDGE('',*,*,#3685,.F.); -#3710 = PLANE('',#3711); -#3711 = AXIS2_PLACEMENT_3D('',#3712,#3713,#3714); -#3712 = CARTESIAN_POINT('',(-8.055555555556,17.9,8.)); -#3713 = DIRECTION('',(0.,0.,1.)); -#3714 = DIRECTION('',(1.,0.,-0.)); -#3715 = ADVANCED_FACE('',(#3716),#3722,.T.); -#3716 = FACE_BOUND('',#3717,.T.); -#3717 = EDGE_LOOP('',(#3718,#3719,#3720,#3721)); -#3718 = ORIENTED_EDGE('',*,*,#3609,.F.); -#3719 = ORIENTED_EDGE('',*,*,#3670,.T.); -#3720 = ORIENTED_EDGE('',*,*,#3649,.T.); -#3721 = ORIENTED_EDGE('',*,*,#3692,.F.); -#3722 = PLANE('',#3723); -#3723 = AXIS2_PLACEMENT_3D('',#3724,#3725,#3726); -#3724 = CARTESIAN_POINT('',(-8.055555555556,17.9,40.)); -#3725 = DIRECTION('',(0.,0.,1.)); -#3726 = DIRECTION('',(1.,0.,-0.)); -#3727 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3731)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#3728,#3729,#3730)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#3728 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#3729 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#3730 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#3731 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3728, - 'distance_accuracy_value','confusion accuracy'); -#3732 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3733,#3735); -#3733 = ( REPRESENTATION_RELATIONSHIP('','',#3576,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3734) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#3734 = ITEM_DEFINED_TRANSFORMATION('','',#11,#95); -#3735 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #3736); -#3736 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('21','WireDuct_RightCombSlot_09', - '',#5,#3571,$); -#3737 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3573)); -#3738 = SHAPE_DEFINITION_REPRESENTATION(#3739,#3745); -#3739 = PRODUCT_DEFINITION_SHAPE('','',#3740); -#3740 = PRODUCT_DEFINITION('design','',#3741,#3744); -#3741 = PRODUCT_DEFINITION_FORMATION('','',#3742); -#3742 = PRODUCT('WireDuct_LeftCombSlot_10','WireDuct_LeftCombSlot_10','' - ,(#3743)); -#3743 = PRODUCT_CONTEXT('',#2,'mechanical'); -#3744 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#3745 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3746),#3896); -#3746 = MANIFOLD_SOLID_BREP('',#3747); -#3747 = CLOSED_SHELL('',(#3748,#3788,#3828,#3850,#3872,#3884)); -#3748 = ADVANCED_FACE('',(#3749),#3783,.F.); -#3749 = FACE_BOUND('',#3750,.F.); -#3750 = EDGE_LOOP('',(#3751,#3761,#3769,#3777)); -#3751 = ORIENTED_EDGE('',*,*,#3752,.F.); -#3752 = EDGE_CURVE('',#3753,#3755,#3757,.T.); -#3753 = VERTEX_POINT('',#3754); -#3754 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3755 = VERTEX_POINT('',#3756); -#3756 = CARTESIAN_POINT('',(3.055555555556,-20.1,40.)); -#3757 = LINE('',#3758,#3759); -#3758 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3759 = VECTOR('',#3760,1.); -#3760 = DIRECTION('',(0.,0.,1.)); -#3761 = ORIENTED_EDGE('',*,*,#3762,.T.); -#3762 = EDGE_CURVE('',#3753,#3763,#3765,.T.); -#3763 = VERTEX_POINT('',#3764); -#3764 = CARTESIAN_POINT('',(3.055555555556,-17.9,8.)); -#3765 = LINE('',#3766,#3767); -#3766 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3767 = VECTOR('',#3768,1.); -#3768 = DIRECTION('',(-0.,1.,0.)); -#3769 = ORIENTED_EDGE('',*,*,#3770,.T.); -#3770 = EDGE_CURVE('',#3763,#3771,#3773,.T.); -#3771 = VERTEX_POINT('',#3772); -#3772 = CARTESIAN_POINT('',(3.055555555556,-17.9,40.)); -#3773 = LINE('',#3774,#3775); -#3774 = CARTESIAN_POINT('',(3.055555555556,-17.9,8.)); -#3775 = VECTOR('',#3776,1.); -#3776 = DIRECTION('',(0.,0.,1.)); -#3777 = ORIENTED_EDGE('',*,*,#3778,.F.); -#3778 = EDGE_CURVE('',#3755,#3771,#3779,.T.); -#3779 = LINE('',#3780,#3781); -#3780 = CARTESIAN_POINT('',(3.055555555556,-20.1,40.)); -#3781 = VECTOR('',#3782,1.); -#3782 = DIRECTION('',(-0.,1.,0.)); -#3783 = PLANE('',#3784); -#3784 = AXIS2_PLACEMENT_3D('',#3785,#3786,#3787); -#3785 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3786 = DIRECTION('',(1.,0.,-0.)); -#3787 = DIRECTION('',(0.,0.,1.)); -#3788 = ADVANCED_FACE('',(#3789),#3823,.T.); -#3789 = FACE_BOUND('',#3790,.T.); -#3790 = EDGE_LOOP('',(#3791,#3801,#3809,#3817)); -#3791 = ORIENTED_EDGE('',*,*,#3792,.F.); -#3792 = EDGE_CURVE('',#3793,#3795,#3797,.T.); -#3793 = VERTEX_POINT('',#3794); -#3794 = CARTESIAN_POINT('',(8.055555555556,-20.1,8.)); -#3795 = VERTEX_POINT('',#3796); -#3796 = CARTESIAN_POINT('',(8.055555555556,-20.1,40.)); -#3797 = LINE('',#3798,#3799); -#3798 = CARTESIAN_POINT('',(8.055555555556,-20.1,8.)); -#3799 = VECTOR('',#3800,1.); -#3800 = DIRECTION('',(0.,0.,1.)); -#3801 = ORIENTED_EDGE('',*,*,#3802,.T.); -#3802 = EDGE_CURVE('',#3793,#3803,#3805,.T.); -#3803 = VERTEX_POINT('',#3804); -#3804 = CARTESIAN_POINT('',(8.055555555556,-17.9,8.)); -#3805 = LINE('',#3806,#3807); -#3806 = CARTESIAN_POINT('',(8.055555555556,-20.1,8.)); -#3807 = VECTOR('',#3808,1.); -#3808 = DIRECTION('',(-0.,1.,0.)); -#3809 = ORIENTED_EDGE('',*,*,#3810,.T.); -#3810 = EDGE_CURVE('',#3803,#3811,#3813,.T.); -#3811 = VERTEX_POINT('',#3812); -#3812 = CARTESIAN_POINT('',(8.055555555556,-17.9,40.)); -#3813 = LINE('',#3814,#3815); -#3814 = CARTESIAN_POINT('',(8.055555555556,-17.9,8.)); -#3815 = VECTOR('',#3816,1.); -#3816 = DIRECTION('',(0.,0.,1.)); -#3817 = ORIENTED_EDGE('',*,*,#3818,.F.); -#3818 = EDGE_CURVE('',#3795,#3811,#3819,.T.); -#3819 = LINE('',#3820,#3821); -#3820 = CARTESIAN_POINT('',(8.055555555556,-20.1,40.)); -#3821 = VECTOR('',#3822,1.); -#3822 = DIRECTION('',(-0.,1.,0.)); -#3823 = PLANE('',#3824); -#3824 = AXIS2_PLACEMENT_3D('',#3825,#3826,#3827); -#3825 = CARTESIAN_POINT('',(8.055555555556,-20.1,8.)); -#3826 = DIRECTION('',(1.,0.,-0.)); -#3827 = DIRECTION('',(0.,0.,1.)); -#3828 = ADVANCED_FACE('',(#3829),#3845,.F.); -#3829 = FACE_BOUND('',#3830,.F.); -#3830 = EDGE_LOOP('',(#3831,#3837,#3838,#3844)); -#3831 = ORIENTED_EDGE('',*,*,#3832,.F.); -#3832 = EDGE_CURVE('',#3753,#3793,#3833,.T.); -#3833 = LINE('',#3834,#3835); -#3834 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3835 = VECTOR('',#3836,1.); -#3836 = DIRECTION('',(1.,0.,-0.)); -#3837 = ORIENTED_EDGE('',*,*,#3752,.T.); -#3838 = ORIENTED_EDGE('',*,*,#3839,.T.); -#3839 = EDGE_CURVE('',#3755,#3795,#3840,.T.); -#3840 = LINE('',#3841,#3842); -#3841 = CARTESIAN_POINT('',(3.055555555556,-20.1,40.)); -#3842 = VECTOR('',#3843,1.); -#3843 = DIRECTION('',(1.,0.,-0.)); -#3844 = ORIENTED_EDGE('',*,*,#3792,.F.); -#3845 = PLANE('',#3846); -#3846 = AXIS2_PLACEMENT_3D('',#3847,#3848,#3849); -#3847 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3848 = DIRECTION('',(-0.,1.,0.)); -#3849 = DIRECTION('',(0.,0.,1.)); -#3850 = ADVANCED_FACE('',(#3851),#3867,.T.); -#3851 = FACE_BOUND('',#3852,.T.); -#3852 = EDGE_LOOP('',(#3853,#3859,#3860,#3866)); -#3853 = ORIENTED_EDGE('',*,*,#3854,.F.); -#3854 = EDGE_CURVE('',#3763,#3803,#3855,.T.); -#3855 = LINE('',#3856,#3857); -#3856 = CARTESIAN_POINT('',(3.055555555556,-17.9,8.)); -#3857 = VECTOR('',#3858,1.); -#3858 = DIRECTION('',(1.,0.,-0.)); -#3859 = ORIENTED_EDGE('',*,*,#3770,.T.); -#3860 = ORIENTED_EDGE('',*,*,#3861,.T.); -#3861 = EDGE_CURVE('',#3771,#3811,#3862,.T.); -#3862 = LINE('',#3863,#3864); -#3863 = CARTESIAN_POINT('',(3.055555555556,-17.9,40.)); -#3864 = VECTOR('',#3865,1.); -#3865 = DIRECTION('',(1.,0.,-0.)); -#3866 = ORIENTED_EDGE('',*,*,#3810,.F.); -#3867 = PLANE('',#3868); -#3868 = AXIS2_PLACEMENT_3D('',#3869,#3870,#3871); -#3869 = CARTESIAN_POINT('',(3.055555555556,-17.9,8.)); -#3870 = DIRECTION('',(-0.,1.,0.)); -#3871 = DIRECTION('',(0.,0.,1.)); -#3872 = ADVANCED_FACE('',(#3873),#3879,.F.); -#3873 = FACE_BOUND('',#3874,.F.); -#3874 = EDGE_LOOP('',(#3875,#3876,#3877,#3878)); -#3875 = ORIENTED_EDGE('',*,*,#3762,.F.); -#3876 = ORIENTED_EDGE('',*,*,#3832,.T.); -#3877 = ORIENTED_EDGE('',*,*,#3802,.T.); -#3878 = ORIENTED_EDGE('',*,*,#3854,.F.); -#3879 = PLANE('',#3880); -#3880 = AXIS2_PLACEMENT_3D('',#3881,#3882,#3883); -#3881 = CARTESIAN_POINT('',(3.055555555556,-20.1,8.)); -#3882 = DIRECTION('',(0.,0.,1.)); -#3883 = DIRECTION('',(1.,0.,-0.)); -#3884 = ADVANCED_FACE('',(#3885),#3891,.T.); -#3885 = FACE_BOUND('',#3886,.T.); -#3886 = EDGE_LOOP('',(#3887,#3888,#3889,#3890)); -#3887 = ORIENTED_EDGE('',*,*,#3778,.F.); -#3888 = ORIENTED_EDGE('',*,*,#3839,.T.); -#3889 = ORIENTED_EDGE('',*,*,#3818,.T.); -#3890 = ORIENTED_EDGE('',*,*,#3861,.F.); -#3891 = PLANE('',#3892); -#3892 = AXIS2_PLACEMENT_3D('',#3893,#3894,#3895); -#3893 = CARTESIAN_POINT('',(3.055555555556,-20.1,40.)); -#3894 = DIRECTION('',(0.,0.,1.)); -#3895 = DIRECTION('',(1.,0.,-0.)); -#3896 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3900)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#3897,#3898,#3899)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#3897 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#3898 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#3899 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#3900 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3897, - 'distance_accuracy_value','confusion accuracy'); -#3901 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3902,#3904); -#3902 = ( REPRESENTATION_RELATIONSHIP('','',#3745,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3903) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#3903 = ITEM_DEFINED_TRANSFORMATION('','',#11,#99); -#3904 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #3905); -#3905 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('22','WireDuct_LeftCombSlot_10', - '',#5,#3740,$); -#3906 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3742)); -#3907 = SHAPE_DEFINITION_REPRESENTATION(#3908,#3914); -#3908 = PRODUCT_DEFINITION_SHAPE('','',#3909); -#3909 = PRODUCT_DEFINITION('design','',#3910,#3913); -#3910 = PRODUCT_DEFINITION_FORMATION('','',#3911); -#3911 = PRODUCT('WireDuct_RightCombSlot_10','WireDuct_RightCombSlot_10', - '',(#3912)); -#3912 = PRODUCT_CONTEXT('',#2,'mechanical'); -#3913 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#3914 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3915),#4065); -#3915 = MANIFOLD_SOLID_BREP('',#3916); -#3916 = CLOSED_SHELL('',(#3917,#3957,#3997,#4019,#4041,#4053)); -#3917 = ADVANCED_FACE('',(#3918),#3952,.F.); -#3918 = FACE_BOUND('',#3919,.F.); -#3919 = EDGE_LOOP('',(#3920,#3930,#3938,#3946)); -#3920 = ORIENTED_EDGE('',*,*,#3921,.F.); -#3921 = EDGE_CURVE('',#3922,#3924,#3926,.T.); -#3922 = VERTEX_POINT('',#3923); -#3923 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#3924 = VERTEX_POINT('',#3925); -#3925 = CARTESIAN_POINT('',(3.055555555556,17.9,40.)); -#3926 = LINE('',#3927,#3928); -#3927 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#3928 = VECTOR('',#3929,1.); -#3929 = DIRECTION('',(0.,0.,1.)); -#3930 = ORIENTED_EDGE('',*,*,#3931,.T.); -#3931 = EDGE_CURVE('',#3922,#3932,#3934,.T.); -#3932 = VERTEX_POINT('',#3933); -#3933 = CARTESIAN_POINT('',(3.055555555556,20.1,8.)); -#3934 = LINE('',#3935,#3936); -#3935 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#3936 = VECTOR('',#3937,1.); -#3937 = DIRECTION('',(-0.,1.,0.)); -#3938 = ORIENTED_EDGE('',*,*,#3939,.T.); -#3939 = EDGE_CURVE('',#3932,#3940,#3942,.T.); -#3940 = VERTEX_POINT('',#3941); -#3941 = CARTESIAN_POINT('',(3.055555555556,20.1,40.)); -#3942 = LINE('',#3943,#3944); -#3943 = CARTESIAN_POINT('',(3.055555555556,20.1,8.)); -#3944 = VECTOR('',#3945,1.); -#3945 = DIRECTION('',(0.,0.,1.)); -#3946 = ORIENTED_EDGE('',*,*,#3947,.F.); -#3947 = EDGE_CURVE('',#3924,#3940,#3948,.T.); -#3948 = LINE('',#3949,#3950); -#3949 = CARTESIAN_POINT('',(3.055555555556,17.9,40.)); -#3950 = VECTOR('',#3951,1.); -#3951 = DIRECTION('',(-0.,1.,0.)); -#3952 = PLANE('',#3953); -#3953 = AXIS2_PLACEMENT_3D('',#3954,#3955,#3956); -#3954 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#3955 = DIRECTION('',(1.,0.,-0.)); -#3956 = DIRECTION('',(0.,0.,1.)); -#3957 = ADVANCED_FACE('',(#3958),#3992,.T.); -#3958 = FACE_BOUND('',#3959,.T.); -#3959 = EDGE_LOOP('',(#3960,#3970,#3978,#3986)); -#3960 = ORIENTED_EDGE('',*,*,#3961,.F.); -#3961 = EDGE_CURVE('',#3962,#3964,#3966,.T.); -#3962 = VERTEX_POINT('',#3963); -#3963 = CARTESIAN_POINT('',(8.055555555556,17.9,8.)); -#3964 = VERTEX_POINT('',#3965); -#3965 = CARTESIAN_POINT('',(8.055555555556,17.9,40.)); -#3966 = LINE('',#3967,#3968); -#3967 = CARTESIAN_POINT('',(8.055555555556,17.9,8.)); -#3968 = VECTOR('',#3969,1.); -#3969 = DIRECTION('',(0.,0.,1.)); -#3970 = ORIENTED_EDGE('',*,*,#3971,.T.); -#3971 = EDGE_CURVE('',#3962,#3972,#3974,.T.); -#3972 = VERTEX_POINT('',#3973); -#3973 = CARTESIAN_POINT('',(8.055555555556,20.1,8.)); -#3974 = LINE('',#3975,#3976); -#3975 = CARTESIAN_POINT('',(8.055555555556,17.9,8.)); -#3976 = VECTOR('',#3977,1.); -#3977 = DIRECTION('',(-0.,1.,0.)); -#3978 = ORIENTED_EDGE('',*,*,#3979,.T.); -#3979 = EDGE_CURVE('',#3972,#3980,#3982,.T.); -#3980 = VERTEX_POINT('',#3981); -#3981 = CARTESIAN_POINT('',(8.055555555556,20.1,40.)); -#3982 = LINE('',#3983,#3984); -#3983 = CARTESIAN_POINT('',(8.055555555556,20.1,8.)); -#3984 = VECTOR('',#3985,1.); -#3985 = DIRECTION('',(0.,0.,1.)); -#3986 = ORIENTED_EDGE('',*,*,#3987,.F.); -#3987 = EDGE_CURVE('',#3964,#3980,#3988,.T.); -#3988 = LINE('',#3989,#3990); -#3989 = CARTESIAN_POINT('',(8.055555555556,17.9,40.)); -#3990 = VECTOR('',#3991,1.); -#3991 = DIRECTION('',(-0.,1.,0.)); +#3702 = ORIENTED_EDGE('',*,*,#3383,.T.); +#3703 = ORIENTED_EDGE('',*,*,#3665,.T.); +#3704 = PLANE('',#3705); +#3705 = AXIS2_PLACEMENT_3D('',#3706,#3707,#3708); +#3706 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3707 = DIRECTION('',(0.,0.,1.)); +#3708 = DIRECTION('',(1.,0.,-0.)); +#3709 = ADVANCED_FACE('',(#3710),#3728,.F.); +#3710 = FACE_BOUND('',#3711,.F.); +#3711 = EDGE_LOOP('',(#3712,#3713,#3714,#3722)); +#3712 = ORIENTED_EDGE('',*,*,#3697,.F.); +#3713 = ORIENTED_EDGE('',*,*,#3375,.T.); +#3714 = ORIENTED_EDGE('',*,*,#3715,.T.); +#3715 = EDGE_CURVE('',#3368,#3716,#3718,.T.); +#3716 = VERTEX_POINT('',#3717); +#3717 = CARTESIAN_POINT('',(85.833333333333,20.,8.)); +#3718 = LINE('',#3719,#3720); +#3719 = CARTESIAN_POINT('',(85.833333333333,17.8,8.)); +#3720 = VECTOR('',#3721,1.); +#3721 = DIRECTION('',(-0.,1.,0.)); +#3722 = ORIENTED_EDGE('',*,*,#3723,.F.); +#3723 = EDGE_CURVE('',#3690,#3716,#3724,.T.); +#3724 = LINE('',#3725,#3726); +#3725 = CARTESIAN_POINT('',(85.833333333333,20.,4.)); +#3726 = VECTOR('',#3727,1.); +#3727 = DIRECTION('',(-0.,0.,-1.)); +#3728 = PLANE('',#3729); +#3729 = AXIS2_PLACEMENT_3D('',#3730,#3731,#3732); +#3730 = CARTESIAN_POINT('',(85.833333333333,17.8,8.)); +#3731 = DIRECTION('',(1.,0.,-0.)); +#3732 = DIRECTION('',(0.,0.,1.)); +#3733 = ADVANCED_FACE('',(#3734),#3752,.T.); +#3734 = FACE_BOUND('',#3735,.T.); +#3735 = EDGE_LOOP('',(#3736,#3744,#3745,#3746)); +#3736 = ORIENTED_EDGE('',*,*,#3737,.F.); +#3737 = EDGE_CURVE('',#3360,#3738,#3740,.T.); +#3738 = VERTEX_POINT('',#3739); +#3739 = CARTESIAN_POINT('',(80.833333333333,20.,8.)); +#3740 = LINE('',#3741,#3742); +#3741 = CARTESIAN_POINT('',(80.833333333333,17.8,8.)); +#3742 = VECTOR('',#3743,1.); +#3743 = DIRECTION('',(-0.,1.,0.)); +#3744 = ORIENTED_EDGE('',*,*,#3367,.T.); +#3745 = ORIENTED_EDGE('',*,*,#3715,.T.); +#3746 = ORIENTED_EDGE('',*,*,#3747,.F.); +#3747 = EDGE_CURVE('',#3738,#3716,#3748,.T.); +#3748 = LINE('',#3749,#3750); +#3749 = CARTESIAN_POINT('',(-9.583333333333,20.,8.)); +#3750 = VECTOR('',#3751,1.); +#3751 = DIRECTION('',(1.,0.,-0.)); +#3752 = PLANE('',#3753); +#3753 = AXIS2_PLACEMENT_3D('',#3754,#3755,#3756); +#3754 = CARTESIAN_POINT('',(80.833333333333,17.8,8.)); +#3755 = DIRECTION('',(0.,0.,1.)); +#3756 = DIRECTION('',(1.,0.,-0.)); +#3757 = ADVANCED_FACE('',(#3758),#3776,.T.); +#3758 = FACE_BOUND('',#3759,.T.); +#3759 = EDGE_LOOP('',(#3760,#3768,#3769,#3770)); +#3760 = ORIENTED_EDGE('',*,*,#3761,.F.); +#3761 = EDGE_CURVE('',#3352,#3762,#3764,.T.); +#3762 = VERTEX_POINT('',#3763); +#3763 = CARTESIAN_POINT('',(80.833333333333,20.,40.)); +#3764 = LINE('',#3765,#3766); +#3765 = CARTESIAN_POINT('',(80.833333333333,17.8,40.)); +#3766 = VECTOR('',#3767,1.); +#3767 = DIRECTION('',(-0.,1.,0.)); +#3768 = ORIENTED_EDGE('',*,*,#3359,.T.); +#3769 = ORIENTED_EDGE('',*,*,#3737,.T.); +#3770 = ORIENTED_EDGE('',*,*,#3771,.F.); +#3771 = EDGE_CURVE('',#3762,#3738,#3772,.T.); +#3772 = LINE('',#3773,#3774); +#3773 = CARTESIAN_POINT('',(80.833333333333,20.,4.)); +#3774 = VECTOR('',#3775,1.); +#3775 = DIRECTION('',(-0.,0.,-1.)); +#3776 = PLANE('',#3777); +#3777 = AXIS2_PLACEMENT_3D('',#3778,#3779,#3780); +#3778 = CARTESIAN_POINT('',(80.833333333333,17.8,8.)); +#3779 = DIRECTION('',(1.,0.,-0.)); +#3780 = DIRECTION('',(0.,0.,1.)); +#3781 = ADVANCED_FACE('',(#3782),#3800,.T.); +#3782 = FACE_BOUND('',#3783,.T.); +#3783 = EDGE_LOOP('',(#3784,#3792,#3798,#3799)); +#3784 = ORIENTED_EDGE('',*,*,#3785,.F.); +#3785 = EDGE_CURVE('',#3786,#3762,#3788,.T.); +#3786 = VERTEX_POINT('',#3787); +#3787 = CARTESIAN_POINT('',(74.722222222222,20.,40.)); +#3788 = LINE('',#3789,#3790); +#3789 = CARTESIAN_POINT('',(-100.,20.,40.)); +#3790 = VECTOR('',#3791,1.); +#3791 = DIRECTION('',(1.,0.,-0.)); +#3792 = ORIENTED_EDGE('',*,*,#3793,.F.); +#3793 = EDGE_CURVE('',#3344,#3786,#3794,.T.); +#3794 = LINE('',#3795,#3796); +#3795 = CARTESIAN_POINT('',(74.722222222222,17.8,40.)); +#3796 = VECTOR('',#3797,1.); +#3797 = DIRECTION('',(-0.,1.,0.)); +#3798 = ORIENTED_EDGE('',*,*,#3351,.T.); +#3799 = ORIENTED_EDGE('',*,*,#3761,.T.); +#3800 = PLANE('',#3801); +#3801 = AXIS2_PLACEMENT_3D('',#3802,#3803,#3804); +#3802 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3803 = DIRECTION('',(0.,0.,1.)); +#3804 = DIRECTION('',(1.,0.,-0.)); +#3805 = ADVANCED_FACE('',(#3806),#3824,.F.); +#3806 = FACE_BOUND('',#3807,.F.); +#3807 = EDGE_LOOP('',(#3808,#3809,#3810,#3818)); +#3808 = ORIENTED_EDGE('',*,*,#3793,.F.); +#3809 = ORIENTED_EDGE('',*,*,#3343,.T.); +#3810 = ORIENTED_EDGE('',*,*,#3811,.T.); +#3811 = EDGE_CURVE('',#3336,#3812,#3814,.T.); +#3812 = VERTEX_POINT('',#3813); +#3813 = CARTESIAN_POINT('',(74.722222222222,20.,8.)); +#3814 = LINE('',#3815,#3816); +#3815 = CARTESIAN_POINT('',(74.722222222222,17.8,8.)); +#3816 = VECTOR('',#3817,1.); +#3817 = DIRECTION('',(-0.,1.,0.)); +#3818 = ORIENTED_EDGE('',*,*,#3819,.F.); +#3819 = EDGE_CURVE('',#3786,#3812,#3820,.T.); +#3820 = LINE('',#3821,#3822); +#3821 = CARTESIAN_POINT('',(74.722222222222,20.,4.)); +#3822 = VECTOR('',#3823,1.); +#3823 = DIRECTION('',(-0.,0.,-1.)); +#3824 = PLANE('',#3825); +#3825 = AXIS2_PLACEMENT_3D('',#3826,#3827,#3828); +#3826 = CARTESIAN_POINT('',(74.722222222222,17.8,8.)); +#3827 = DIRECTION('',(1.,0.,-0.)); +#3828 = DIRECTION('',(0.,0.,1.)); +#3829 = ADVANCED_FACE('',(#3830),#3848,.T.); +#3830 = FACE_BOUND('',#3831,.T.); +#3831 = EDGE_LOOP('',(#3832,#3840,#3841,#3842)); +#3832 = ORIENTED_EDGE('',*,*,#3833,.F.); +#3833 = EDGE_CURVE('',#3328,#3834,#3836,.T.); +#3834 = VERTEX_POINT('',#3835); +#3835 = CARTESIAN_POINT('',(69.722222222222,20.,8.)); +#3836 = LINE('',#3837,#3838); +#3837 = CARTESIAN_POINT('',(69.722222222222,17.8,8.)); +#3838 = VECTOR('',#3839,1.); +#3839 = DIRECTION('',(-0.,1.,0.)); +#3840 = ORIENTED_EDGE('',*,*,#3335,.T.); +#3841 = ORIENTED_EDGE('',*,*,#3811,.T.); +#3842 = ORIENTED_EDGE('',*,*,#3843,.F.); +#3843 = EDGE_CURVE('',#3834,#3812,#3844,.T.); +#3844 = LINE('',#3845,#3846); +#3845 = CARTESIAN_POINT('',(-15.13888888888,20.,8.)); +#3846 = VECTOR('',#3847,1.); +#3847 = DIRECTION('',(1.,0.,-0.)); +#3848 = PLANE('',#3849); +#3849 = AXIS2_PLACEMENT_3D('',#3850,#3851,#3852); +#3850 = CARTESIAN_POINT('',(69.722222222222,17.8,8.)); +#3851 = DIRECTION('',(0.,0.,1.)); +#3852 = DIRECTION('',(1.,0.,-0.)); +#3853 = ADVANCED_FACE('',(#3854),#3872,.T.); +#3854 = FACE_BOUND('',#3855,.T.); +#3855 = EDGE_LOOP('',(#3856,#3864,#3865,#3866)); +#3856 = ORIENTED_EDGE('',*,*,#3857,.F.); +#3857 = EDGE_CURVE('',#3320,#3858,#3860,.T.); +#3858 = VERTEX_POINT('',#3859); +#3859 = CARTESIAN_POINT('',(69.722222222222,20.,40.)); +#3860 = LINE('',#3861,#3862); +#3861 = CARTESIAN_POINT('',(69.722222222222,17.8,40.)); +#3862 = VECTOR('',#3863,1.); +#3863 = DIRECTION('',(-0.,1.,0.)); +#3864 = ORIENTED_EDGE('',*,*,#3327,.T.); +#3865 = ORIENTED_EDGE('',*,*,#3833,.T.); +#3866 = ORIENTED_EDGE('',*,*,#3867,.F.); +#3867 = EDGE_CURVE('',#3858,#3834,#3868,.T.); +#3868 = LINE('',#3869,#3870); +#3869 = CARTESIAN_POINT('',(69.722222222222,20.,4.)); +#3870 = VECTOR('',#3871,1.); +#3871 = DIRECTION('',(-0.,0.,-1.)); +#3872 = PLANE('',#3873); +#3873 = AXIS2_PLACEMENT_3D('',#3874,#3875,#3876); +#3874 = CARTESIAN_POINT('',(69.722222222222,17.8,8.)); +#3875 = DIRECTION('',(1.,0.,-0.)); +#3876 = DIRECTION('',(0.,0.,1.)); +#3877 = ADVANCED_FACE('',(#3878),#3896,.T.); +#3878 = FACE_BOUND('',#3879,.T.); +#3879 = EDGE_LOOP('',(#3880,#3888,#3894,#3895)); +#3880 = ORIENTED_EDGE('',*,*,#3881,.F.); +#3881 = EDGE_CURVE('',#3882,#3858,#3884,.T.); +#3882 = VERTEX_POINT('',#3883); +#3883 = CARTESIAN_POINT('',(63.611111111111,20.,40.)); +#3884 = LINE('',#3885,#3886); +#3885 = CARTESIAN_POINT('',(-100.,20.,40.)); +#3886 = VECTOR('',#3887,1.); +#3887 = DIRECTION('',(1.,0.,-0.)); +#3888 = ORIENTED_EDGE('',*,*,#3889,.F.); +#3889 = EDGE_CURVE('',#3312,#3882,#3890,.T.); +#3890 = LINE('',#3891,#3892); +#3891 = CARTESIAN_POINT('',(63.611111111111,17.8,40.)); +#3892 = VECTOR('',#3893,1.); +#3893 = DIRECTION('',(-0.,1.,0.)); +#3894 = ORIENTED_EDGE('',*,*,#3319,.T.); +#3895 = ORIENTED_EDGE('',*,*,#3857,.T.); +#3896 = PLANE('',#3897); +#3897 = AXIS2_PLACEMENT_3D('',#3898,#3899,#3900); +#3898 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3899 = DIRECTION('',(0.,0.,1.)); +#3900 = DIRECTION('',(1.,0.,-0.)); +#3901 = ADVANCED_FACE('',(#3902),#3920,.F.); +#3902 = FACE_BOUND('',#3903,.F.); +#3903 = EDGE_LOOP('',(#3904,#3905,#3906,#3914)); +#3904 = ORIENTED_EDGE('',*,*,#3889,.F.); +#3905 = ORIENTED_EDGE('',*,*,#3311,.T.); +#3906 = ORIENTED_EDGE('',*,*,#3907,.T.); +#3907 = EDGE_CURVE('',#3304,#3908,#3910,.T.); +#3908 = VERTEX_POINT('',#3909); +#3909 = CARTESIAN_POINT('',(63.611111111111,20.,8.)); +#3910 = LINE('',#3911,#3912); +#3911 = CARTESIAN_POINT('',(63.611111111111,17.8,8.)); +#3912 = VECTOR('',#3913,1.); +#3913 = DIRECTION('',(-0.,1.,0.)); +#3914 = ORIENTED_EDGE('',*,*,#3915,.F.); +#3915 = EDGE_CURVE('',#3882,#3908,#3916,.T.); +#3916 = LINE('',#3917,#3918); +#3917 = CARTESIAN_POINT('',(63.611111111111,20.,4.)); +#3918 = VECTOR('',#3919,1.); +#3919 = DIRECTION('',(-0.,0.,-1.)); +#3920 = PLANE('',#3921); +#3921 = AXIS2_PLACEMENT_3D('',#3922,#3923,#3924); +#3922 = CARTESIAN_POINT('',(63.611111111111,17.8,8.)); +#3923 = DIRECTION('',(1.,0.,-0.)); +#3924 = DIRECTION('',(0.,0.,1.)); +#3925 = ADVANCED_FACE('',(#3926),#3944,.T.); +#3926 = FACE_BOUND('',#3927,.T.); +#3927 = EDGE_LOOP('',(#3928,#3936,#3937,#3938)); +#3928 = ORIENTED_EDGE('',*,*,#3929,.F.); +#3929 = EDGE_CURVE('',#3296,#3930,#3932,.T.); +#3930 = VERTEX_POINT('',#3931); +#3931 = CARTESIAN_POINT('',(58.611111111111,20.,8.)); +#3932 = LINE('',#3933,#3934); +#3933 = CARTESIAN_POINT('',(58.611111111111,17.8,8.)); +#3934 = VECTOR('',#3935,1.); +#3935 = DIRECTION('',(-0.,1.,0.)); +#3936 = ORIENTED_EDGE('',*,*,#3303,.T.); +#3937 = ORIENTED_EDGE('',*,*,#3907,.T.); +#3938 = ORIENTED_EDGE('',*,*,#3939,.F.); +#3939 = EDGE_CURVE('',#3930,#3908,#3940,.T.); +#3940 = LINE('',#3941,#3942); +#3941 = CARTESIAN_POINT('',(-20.69444444444,20.,8.)); +#3942 = VECTOR('',#3943,1.); +#3943 = DIRECTION('',(1.,0.,-0.)); +#3944 = PLANE('',#3945); +#3945 = AXIS2_PLACEMENT_3D('',#3946,#3947,#3948); +#3946 = CARTESIAN_POINT('',(58.611111111111,17.8,8.)); +#3947 = DIRECTION('',(0.,0.,1.)); +#3948 = DIRECTION('',(1.,0.,-0.)); +#3949 = ADVANCED_FACE('',(#3950),#3968,.T.); +#3950 = FACE_BOUND('',#3951,.T.); +#3951 = EDGE_LOOP('',(#3952,#3960,#3961,#3962)); +#3952 = ORIENTED_EDGE('',*,*,#3953,.F.); +#3953 = EDGE_CURVE('',#3288,#3954,#3956,.T.); +#3954 = VERTEX_POINT('',#3955); +#3955 = CARTESIAN_POINT('',(58.611111111111,20.,40.)); +#3956 = LINE('',#3957,#3958); +#3957 = CARTESIAN_POINT('',(58.611111111111,17.8,40.)); +#3958 = VECTOR('',#3959,1.); +#3959 = DIRECTION('',(-0.,1.,0.)); +#3960 = ORIENTED_EDGE('',*,*,#3295,.T.); +#3961 = ORIENTED_EDGE('',*,*,#3929,.T.); +#3962 = ORIENTED_EDGE('',*,*,#3963,.F.); +#3963 = EDGE_CURVE('',#3954,#3930,#3964,.T.); +#3964 = LINE('',#3965,#3966); +#3965 = CARTESIAN_POINT('',(58.611111111111,20.,4.)); +#3966 = VECTOR('',#3967,1.); +#3967 = DIRECTION('',(-0.,0.,-1.)); +#3968 = PLANE('',#3969); +#3969 = AXIS2_PLACEMENT_3D('',#3970,#3971,#3972); +#3970 = CARTESIAN_POINT('',(58.611111111111,17.8,8.)); +#3971 = DIRECTION('',(1.,0.,-0.)); +#3972 = DIRECTION('',(0.,0.,1.)); +#3973 = ADVANCED_FACE('',(#3974),#3992,.T.); +#3974 = FACE_BOUND('',#3975,.T.); +#3975 = EDGE_LOOP('',(#3976,#3984,#3990,#3991)); +#3976 = ORIENTED_EDGE('',*,*,#3977,.F.); +#3977 = EDGE_CURVE('',#3978,#3954,#3980,.T.); +#3978 = VERTEX_POINT('',#3979); +#3979 = CARTESIAN_POINT('',(52.5,20.,40.)); +#3980 = LINE('',#3981,#3982); +#3981 = CARTESIAN_POINT('',(-100.,20.,40.)); +#3982 = VECTOR('',#3983,1.); +#3983 = DIRECTION('',(1.,0.,-0.)); +#3984 = ORIENTED_EDGE('',*,*,#3985,.F.); +#3985 = EDGE_CURVE('',#3280,#3978,#3986,.T.); +#3986 = LINE('',#3987,#3988); +#3987 = CARTESIAN_POINT('',(52.5,17.8,40.)); +#3988 = VECTOR('',#3989,1.); +#3989 = DIRECTION('',(-0.,1.,0.)); +#3990 = ORIENTED_EDGE('',*,*,#3287,.T.); +#3991 = ORIENTED_EDGE('',*,*,#3953,.T.); #3992 = PLANE('',#3993); #3993 = AXIS2_PLACEMENT_3D('',#3994,#3995,#3996); -#3994 = CARTESIAN_POINT('',(8.055555555556,17.9,8.)); -#3995 = DIRECTION('',(1.,0.,-0.)); -#3996 = DIRECTION('',(0.,0.,1.)); -#3997 = ADVANCED_FACE('',(#3998),#4014,.F.); +#3994 = CARTESIAN_POINT('',(-100.,18.,40.)); +#3995 = DIRECTION('',(0.,0.,1.)); +#3996 = DIRECTION('',(1.,0.,-0.)); +#3997 = ADVANCED_FACE('',(#3998),#4016,.F.); #3998 = FACE_BOUND('',#3999,.F.); -#3999 = EDGE_LOOP('',(#4000,#4006,#4007,#4013)); -#4000 = ORIENTED_EDGE('',*,*,#4001,.F.); -#4001 = EDGE_CURVE('',#3922,#3962,#4002,.T.); -#4002 = LINE('',#4003,#4004); -#4003 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#4004 = VECTOR('',#4005,1.); -#4005 = DIRECTION('',(1.,0.,-0.)); -#4006 = ORIENTED_EDGE('',*,*,#3921,.T.); -#4007 = ORIENTED_EDGE('',*,*,#4008,.T.); -#4008 = EDGE_CURVE('',#3924,#3964,#4009,.T.); -#4009 = LINE('',#4010,#4011); -#4010 = CARTESIAN_POINT('',(3.055555555556,17.9,40.)); -#4011 = VECTOR('',#4012,1.); -#4012 = DIRECTION('',(1.,0.,-0.)); -#4013 = ORIENTED_EDGE('',*,*,#3961,.F.); -#4014 = PLANE('',#4015); -#4015 = AXIS2_PLACEMENT_3D('',#4016,#4017,#4018); -#4016 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#4017 = DIRECTION('',(-0.,1.,0.)); -#4018 = DIRECTION('',(0.,0.,1.)); -#4019 = ADVANCED_FACE('',(#4020),#4036,.T.); -#4020 = FACE_BOUND('',#4021,.T.); -#4021 = EDGE_LOOP('',(#4022,#4028,#4029,#4035)); -#4022 = ORIENTED_EDGE('',*,*,#4023,.F.); -#4023 = EDGE_CURVE('',#3932,#3972,#4024,.T.); -#4024 = LINE('',#4025,#4026); -#4025 = CARTESIAN_POINT('',(3.055555555556,20.1,8.)); -#4026 = VECTOR('',#4027,1.); -#4027 = DIRECTION('',(1.,0.,-0.)); -#4028 = ORIENTED_EDGE('',*,*,#3939,.T.); -#4029 = ORIENTED_EDGE('',*,*,#4030,.T.); -#4030 = EDGE_CURVE('',#3940,#3980,#4031,.T.); -#4031 = LINE('',#4032,#4033); -#4032 = CARTESIAN_POINT('',(3.055555555556,20.1,40.)); -#4033 = VECTOR('',#4034,1.); -#4034 = DIRECTION('',(1.,0.,-0.)); -#4035 = ORIENTED_EDGE('',*,*,#3979,.F.); -#4036 = PLANE('',#4037); -#4037 = AXIS2_PLACEMENT_3D('',#4038,#4039,#4040); -#4038 = CARTESIAN_POINT('',(3.055555555556,20.1,8.)); -#4039 = DIRECTION('',(-0.,1.,0.)); -#4040 = DIRECTION('',(0.,0.,1.)); -#4041 = ADVANCED_FACE('',(#4042),#4048,.F.); -#4042 = FACE_BOUND('',#4043,.F.); -#4043 = EDGE_LOOP('',(#4044,#4045,#4046,#4047)); -#4044 = ORIENTED_EDGE('',*,*,#3931,.F.); -#4045 = ORIENTED_EDGE('',*,*,#4001,.T.); -#4046 = ORIENTED_EDGE('',*,*,#3971,.T.); -#4047 = ORIENTED_EDGE('',*,*,#4023,.F.); -#4048 = PLANE('',#4049); -#4049 = AXIS2_PLACEMENT_3D('',#4050,#4051,#4052); -#4050 = CARTESIAN_POINT('',(3.055555555556,17.9,8.)); -#4051 = DIRECTION('',(0.,0.,1.)); -#4052 = DIRECTION('',(1.,0.,-0.)); -#4053 = ADVANCED_FACE('',(#4054),#4060,.T.); -#4054 = FACE_BOUND('',#4055,.T.); -#4055 = EDGE_LOOP('',(#4056,#4057,#4058,#4059)); -#4056 = ORIENTED_EDGE('',*,*,#3947,.F.); -#4057 = ORIENTED_EDGE('',*,*,#4008,.T.); -#4058 = ORIENTED_EDGE('',*,*,#3987,.T.); -#4059 = ORIENTED_EDGE('',*,*,#4030,.F.); -#4060 = PLANE('',#4061); -#4061 = AXIS2_PLACEMENT_3D('',#4062,#4063,#4064); -#4062 = CARTESIAN_POINT('',(3.055555555556,17.9,40.)); -#4063 = DIRECTION('',(0.,0.,1.)); -#4064 = DIRECTION('',(1.,0.,-0.)); -#4065 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4069)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#4066,#4067,#4068)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#4066 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#4067 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#4068 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#4069 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4066, - 'distance_accuracy_value','confusion accuracy'); -#4070 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4071,#4073); -#4071 = ( REPRESENTATION_RELATIONSHIP('','',#3914,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4072) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#4072 = ITEM_DEFINED_TRANSFORMATION('','',#11,#103); -#4073 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #4074); -#4074 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('23','WireDuct_RightCombSlot_10', - '',#5,#3909,$); -#4075 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3911)); -#4076 = SHAPE_DEFINITION_REPRESENTATION(#4077,#4083); -#4077 = PRODUCT_DEFINITION_SHAPE('','',#4078); -#4078 = PRODUCT_DEFINITION('design','',#4079,#4082); -#4079 = PRODUCT_DEFINITION_FORMATION('','',#4080); -#4080 = PRODUCT('WireDuct_LeftCombSlot_11','WireDuct_LeftCombSlot_11','' - ,(#4081)); -#4081 = PRODUCT_CONTEXT('',#2,'mechanical'); -#4082 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#4083 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4084),#4234); -#4084 = MANIFOLD_SOLID_BREP('',#4085); -#4085 = CLOSED_SHELL('',(#4086,#4126,#4166,#4188,#4210,#4222)); -#4086 = ADVANCED_FACE('',(#4087),#4121,.F.); -#4087 = FACE_BOUND('',#4088,.F.); -#4088 = EDGE_LOOP('',(#4089,#4099,#4107,#4115)); -#4089 = ORIENTED_EDGE('',*,*,#4090,.F.); -#4090 = EDGE_CURVE('',#4091,#4093,#4095,.T.); -#4091 = VERTEX_POINT('',#4092); -#4092 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4093 = VERTEX_POINT('',#4094); -#4094 = CARTESIAN_POINT('',(14.166666666667,-20.1,40.)); -#4095 = LINE('',#4096,#4097); -#4096 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4097 = VECTOR('',#4098,1.); -#4098 = DIRECTION('',(0.,0.,1.)); -#4099 = ORIENTED_EDGE('',*,*,#4100,.T.); -#4100 = EDGE_CURVE('',#4091,#4101,#4103,.T.); -#4101 = VERTEX_POINT('',#4102); -#4102 = CARTESIAN_POINT('',(14.166666666667,-17.9,8.)); -#4103 = LINE('',#4104,#4105); -#4104 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4105 = VECTOR('',#4106,1.); -#4106 = DIRECTION('',(-0.,1.,0.)); -#4107 = ORIENTED_EDGE('',*,*,#4108,.T.); -#4108 = EDGE_CURVE('',#4101,#4109,#4111,.T.); -#4109 = VERTEX_POINT('',#4110); -#4110 = CARTESIAN_POINT('',(14.166666666667,-17.9,40.)); -#4111 = LINE('',#4112,#4113); -#4112 = CARTESIAN_POINT('',(14.166666666667,-17.9,8.)); -#4113 = VECTOR('',#4114,1.); -#4114 = DIRECTION('',(0.,0.,1.)); -#4115 = ORIENTED_EDGE('',*,*,#4116,.F.); -#4116 = EDGE_CURVE('',#4093,#4109,#4117,.T.); -#4117 = LINE('',#4118,#4119); -#4118 = CARTESIAN_POINT('',(14.166666666667,-20.1,40.)); -#4119 = VECTOR('',#4120,1.); -#4120 = DIRECTION('',(-0.,1.,0.)); -#4121 = PLANE('',#4122); -#4122 = AXIS2_PLACEMENT_3D('',#4123,#4124,#4125); -#4123 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4124 = DIRECTION('',(1.,0.,-0.)); -#4125 = DIRECTION('',(0.,0.,1.)); -#4126 = ADVANCED_FACE('',(#4127),#4161,.T.); -#4127 = FACE_BOUND('',#4128,.T.); -#4128 = EDGE_LOOP('',(#4129,#4139,#4147,#4155)); -#4129 = ORIENTED_EDGE('',*,*,#4130,.F.); -#4130 = EDGE_CURVE('',#4131,#4133,#4135,.T.); -#4131 = VERTEX_POINT('',#4132); -#4132 = CARTESIAN_POINT('',(19.166666666667,-20.1,8.)); -#4133 = VERTEX_POINT('',#4134); -#4134 = CARTESIAN_POINT('',(19.166666666667,-20.1,40.)); -#4135 = LINE('',#4136,#4137); -#4136 = CARTESIAN_POINT('',(19.166666666667,-20.1,8.)); -#4137 = VECTOR('',#4138,1.); -#4138 = DIRECTION('',(0.,0.,1.)); -#4139 = ORIENTED_EDGE('',*,*,#4140,.T.); -#4140 = EDGE_CURVE('',#4131,#4141,#4143,.T.); -#4141 = VERTEX_POINT('',#4142); -#4142 = CARTESIAN_POINT('',(19.166666666667,-17.9,8.)); -#4143 = LINE('',#4144,#4145); -#4144 = CARTESIAN_POINT('',(19.166666666667,-20.1,8.)); -#4145 = VECTOR('',#4146,1.); -#4146 = DIRECTION('',(-0.,1.,0.)); -#4147 = ORIENTED_EDGE('',*,*,#4148,.T.); -#4148 = EDGE_CURVE('',#4141,#4149,#4151,.T.); -#4149 = VERTEX_POINT('',#4150); -#4150 = CARTESIAN_POINT('',(19.166666666667,-17.9,40.)); -#4151 = LINE('',#4152,#4153); -#4152 = CARTESIAN_POINT('',(19.166666666667,-17.9,8.)); -#4153 = VECTOR('',#4154,1.); -#4154 = DIRECTION('',(0.,0.,1.)); -#4155 = ORIENTED_EDGE('',*,*,#4156,.F.); -#4156 = EDGE_CURVE('',#4133,#4149,#4157,.T.); -#4157 = LINE('',#4158,#4159); -#4158 = CARTESIAN_POINT('',(19.166666666667,-20.1,40.)); -#4159 = VECTOR('',#4160,1.); -#4160 = DIRECTION('',(-0.,1.,0.)); -#4161 = PLANE('',#4162); -#4162 = AXIS2_PLACEMENT_3D('',#4163,#4164,#4165); -#4163 = CARTESIAN_POINT('',(19.166666666667,-20.1,8.)); -#4164 = DIRECTION('',(1.,0.,-0.)); -#4165 = DIRECTION('',(0.,0.,1.)); -#4166 = ADVANCED_FACE('',(#4167),#4183,.F.); -#4167 = FACE_BOUND('',#4168,.F.); -#4168 = EDGE_LOOP('',(#4169,#4175,#4176,#4182)); -#4169 = ORIENTED_EDGE('',*,*,#4170,.F.); -#4170 = EDGE_CURVE('',#4091,#4131,#4171,.T.); -#4171 = LINE('',#4172,#4173); -#4172 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4173 = VECTOR('',#4174,1.); -#4174 = DIRECTION('',(1.,0.,-0.)); -#4175 = ORIENTED_EDGE('',*,*,#4090,.T.); -#4176 = ORIENTED_EDGE('',*,*,#4177,.T.); -#4177 = EDGE_CURVE('',#4093,#4133,#4178,.T.); +#3999 = EDGE_LOOP('',(#4000,#4001,#4002,#4010)); +#4000 = ORIENTED_EDGE('',*,*,#3985,.F.); +#4001 = ORIENTED_EDGE('',*,*,#3279,.T.); +#4002 = ORIENTED_EDGE('',*,*,#4003,.T.); +#4003 = EDGE_CURVE('',#3272,#4004,#4006,.T.); +#4004 = VERTEX_POINT('',#4005); +#4005 = CARTESIAN_POINT('',(52.5,20.,8.)); +#4006 = LINE('',#4007,#4008); +#4007 = CARTESIAN_POINT('',(52.5,17.8,8.)); +#4008 = VECTOR('',#4009,1.); +#4009 = DIRECTION('',(-0.,1.,0.)); +#4010 = ORIENTED_EDGE('',*,*,#4011,.F.); +#4011 = EDGE_CURVE('',#3978,#4004,#4012,.T.); +#4012 = LINE('',#4013,#4014); +#4013 = CARTESIAN_POINT('',(52.5,20.,4.)); +#4014 = VECTOR('',#4015,1.); +#4015 = DIRECTION('',(-0.,0.,-1.)); +#4016 = PLANE('',#4017); +#4017 = AXIS2_PLACEMENT_3D('',#4018,#4019,#4020); +#4018 = CARTESIAN_POINT('',(52.5,17.8,8.)); +#4019 = DIRECTION('',(1.,0.,-0.)); +#4020 = DIRECTION('',(0.,0.,1.)); +#4021 = ADVANCED_FACE('',(#4022),#4040,.T.); +#4022 = FACE_BOUND('',#4023,.T.); +#4023 = EDGE_LOOP('',(#4024,#4032,#4033,#4034)); +#4024 = ORIENTED_EDGE('',*,*,#4025,.F.); +#4025 = EDGE_CURVE('',#3264,#4026,#4028,.T.); +#4026 = VERTEX_POINT('',#4027); +#4027 = CARTESIAN_POINT('',(47.5,20.,8.)); +#4028 = LINE('',#4029,#4030); +#4029 = CARTESIAN_POINT('',(47.5,17.8,8.)); +#4030 = VECTOR('',#4031,1.); +#4031 = DIRECTION('',(-0.,1.,0.)); +#4032 = ORIENTED_EDGE('',*,*,#3271,.T.); +#4033 = ORIENTED_EDGE('',*,*,#4003,.T.); +#4034 = ORIENTED_EDGE('',*,*,#4035,.F.); +#4035 = EDGE_CURVE('',#4026,#4004,#4036,.T.); +#4036 = LINE('',#4037,#4038); +#4037 = CARTESIAN_POINT('',(-26.25,20.,8.)); +#4038 = VECTOR('',#4039,1.); +#4039 = DIRECTION('',(1.,0.,-0.)); +#4040 = PLANE('',#4041); +#4041 = AXIS2_PLACEMENT_3D('',#4042,#4043,#4044); +#4042 = CARTESIAN_POINT('',(47.5,17.8,8.)); +#4043 = DIRECTION('',(0.,0.,1.)); +#4044 = DIRECTION('',(1.,0.,-0.)); +#4045 = ADVANCED_FACE('',(#4046),#4064,.T.); +#4046 = FACE_BOUND('',#4047,.T.); +#4047 = EDGE_LOOP('',(#4048,#4056,#4057,#4058)); +#4048 = ORIENTED_EDGE('',*,*,#4049,.F.); +#4049 = EDGE_CURVE('',#3256,#4050,#4052,.T.); +#4050 = VERTEX_POINT('',#4051); +#4051 = CARTESIAN_POINT('',(47.5,20.,40.)); +#4052 = LINE('',#4053,#4054); +#4053 = CARTESIAN_POINT('',(47.5,17.8,40.)); +#4054 = VECTOR('',#4055,1.); +#4055 = DIRECTION('',(-0.,1.,0.)); +#4056 = ORIENTED_EDGE('',*,*,#3263,.T.); +#4057 = ORIENTED_EDGE('',*,*,#4025,.T.); +#4058 = ORIENTED_EDGE('',*,*,#4059,.F.); +#4059 = EDGE_CURVE('',#4050,#4026,#4060,.T.); +#4060 = LINE('',#4061,#4062); +#4061 = CARTESIAN_POINT('',(47.5,20.,4.)); +#4062 = VECTOR('',#4063,1.); +#4063 = DIRECTION('',(-0.,0.,-1.)); +#4064 = PLANE('',#4065); +#4065 = AXIS2_PLACEMENT_3D('',#4066,#4067,#4068); +#4066 = CARTESIAN_POINT('',(47.5,17.8,8.)); +#4067 = DIRECTION('',(1.,0.,-0.)); +#4068 = DIRECTION('',(0.,0.,1.)); +#4069 = ADVANCED_FACE('',(#4070),#4088,.T.); +#4070 = FACE_BOUND('',#4071,.T.); +#4071 = EDGE_LOOP('',(#4072,#4080,#4086,#4087)); +#4072 = ORIENTED_EDGE('',*,*,#4073,.F.); +#4073 = EDGE_CURVE('',#4074,#4050,#4076,.T.); +#4074 = VERTEX_POINT('',#4075); +#4075 = CARTESIAN_POINT('',(41.388888888889,20.,40.)); +#4076 = LINE('',#4077,#4078); +#4077 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4078 = VECTOR('',#4079,1.); +#4079 = DIRECTION('',(1.,0.,-0.)); +#4080 = ORIENTED_EDGE('',*,*,#4081,.F.); +#4081 = EDGE_CURVE('',#3248,#4074,#4082,.T.); +#4082 = LINE('',#4083,#4084); +#4083 = CARTESIAN_POINT('',(41.388888888889,17.8,40.)); +#4084 = VECTOR('',#4085,1.); +#4085 = DIRECTION('',(-0.,1.,0.)); +#4086 = ORIENTED_EDGE('',*,*,#3255,.T.); +#4087 = ORIENTED_EDGE('',*,*,#4049,.T.); +#4088 = PLANE('',#4089); +#4089 = AXIS2_PLACEMENT_3D('',#4090,#4091,#4092); +#4090 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4091 = DIRECTION('',(0.,0.,1.)); +#4092 = DIRECTION('',(1.,0.,-0.)); +#4093 = ADVANCED_FACE('',(#4094),#4112,.F.); +#4094 = FACE_BOUND('',#4095,.F.); +#4095 = EDGE_LOOP('',(#4096,#4097,#4098,#4106)); +#4096 = ORIENTED_EDGE('',*,*,#4081,.F.); +#4097 = ORIENTED_EDGE('',*,*,#3247,.T.); +#4098 = ORIENTED_EDGE('',*,*,#4099,.T.); +#4099 = EDGE_CURVE('',#3240,#4100,#4102,.T.); +#4100 = VERTEX_POINT('',#4101); +#4101 = CARTESIAN_POINT('',(41.388888888889,20.,8.)); +#4102 = LINE('',#4103,#4104); +#4103 = CARTESIAN_POINT('',(41.388888888889,17.8,8.)); +#4104 = VECTOR('',#4105,1.); +#4105 = DIRECTION('',(-0.,1.,0.)); +#4106 = ORIENTED_EDGE('',*,*,#4107,.F.); +#4107 = EDGE_CURVE('',#4074,#4100,#4108,.T.); +#4108 = LINE('',#4109,#4110); +#4109 = CARTESIAN_POINT('',(41.388888888889,20.,4.)); +#4110 = VECTOR('',#4111,1.); +#4111 = DIRECTION('',(-0.,0.,-1.)); +#4112 = PLANE('',#4113); +#4113 = AXIS2_PLACEMENT_3D('',#4114,#4115,#4116); +#4114 = CARTESIAN_POINT('',(41.388888888889,17.8,8.)); +#4115 = DIRECTION('',(1.,0.,-0.)); +#4116 = DIRECTION('',(0.,0.,1.)); +#4117 = ADVANCED_FACE('',(#4118),#4136,.T.); +#4118 = FACE_BOUND('',#4119,.T.); +#4119 = EDGE_LOOP('',(#4120,#4128,#4129,#4130)); +#4120 = ORIENTED_EDGE('',*,*,#4121,.F.); +#4121 = EDGE_CURVE('',#3232,#4122,#4124,.T.); +#4122 = VERTEX_POINT('',#4123); +#4123 = CARTESIAN_POINT('',(36.388888888889,20.,8.)); +#4124 = LINE('',#4125,#4126); +#4125 = CARTESIAN_POINT('',(36.388888888889,17.8,8.)); +#4126 = VECTOR('',#4127,1.); +#4127 = DIRECTION('',(-0.,1.,0.)); +#4128 = ORIENTED_EDGE('',*,*,#3239,.T.); +#4129 = ORIENTED_EDGE('',*,*,#4099,.T.); +#4130 = ORIENTED_EDGE('',*,*,#4131,.F.); +#4131 = EDGE_CURVE('',#4122,#4100,#4132,.T.); +#4132 = LINE('',#4133,#4134); +#4133 = CARTESIAN_POINT('',(-31.80555555555,20.,8.)); +#4134 = VECTOR('',#4135,1.); +#4135 = DIRECTION('',(1.,0.,-0.)); +#4136 = PLANE('',#4137); +#4137 = AXIS2_PLACEMENT_3D('',#4138,#4139,#4140); +#4138 = CARTESIAN_POINT('',(36.388888888889,17.8,8.)); +#4139 = DIRECTION('',(0.,0.,1.)); +#4140 = DIRECTION('',(1.,0.,-0.)); +#4141 = ADVANCED_FACE('',(#4142),#4160,.T.); +#4142 = FACE_BOUND('',#4143,.T.); +#4143 = EDGE_LOOP('',(#4144,#4152,#4153,#4154)); +#4144 = ORIENTED_EDGE('',*,*,#4145,.F.); +#4145 = EDGE_CURVE('',#3224,#4146,#4148,.T.); +#4146 = VERTEX_POINT('',#4147); +#4147 = CARTESIAN_POINT('',(36.388888888889,20.,40.)); +#4148 = LINE('',#4149,#4150); +#4149 = CARTESIAN_POINT('',(36.388888888889,17.8,40.)); +#4150 = VECTOR('',#4151,1.); +#4151 = DIRECTION('',(-0.,1.,0.)); +#4152 = ORIENTED_EDGE('',*,*,#3231,.T.); +#4153 = ORIENTED_EDGE('',*,*,#4121,.T.); +#4154 = ORIENTED_EDGE('',*,*,#4155,.F.); +#4155 = EDGE_CURVE('',#4146,#4122,#4156,.T.); +#4156 = LINE('',#4157,#4158); +#4157 = CARTESIAN_POINT('',(36.388888888889,20.,4.)); +#4158 = VECTOR('',#4159,1.); +#4159 = DIRECTION('',(-0.,0.,-1.)); +#4160 = PLANE('',#4161); +#4161 = AXIS2_PLACEMENT_3D('',#4162,#4163,#4164); +#4162 = CARTESIAN_POINT('',(36.388888888889,17.8,8.)); +#4163 = DIRECTION('',(1.,0.,-0.)); +#4164 = DIRECTION('',(0.,0.,1.)); +#4165 = ADVANCED_FACE('',(#4166),#4184,.T.); +#4166 = FACE_BOUND('',#4167,.T.); +#4167 = EDGE_LOOP('',(#4168,#4176,#4182,#4183)); +#4168 = ORIENTED_EDGE('',*,*,#4169,.F.); +#4169 = EDGE_CURVE('',#4170,#4146,#4172,.T.); +#4170 = VERTEX_POINT('',#4171); +#4171 = CARTESIAN_POINT('',(30.277777777778,20.,40.)); +#4172 = LINE('',#4173,#4174); +#4173 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4174 = VECTOR('',#4175,1.); +#4175 = DIRECTION('',(1.,0.,-0.)); +#4176 = ORIENTED_EDGE('',*,*,#4177,.F.); +#4177 = EDGE_CURVE('',#3216,#4170,#4178,.T.); #4178 = LINE('',#4179,#4180); -#4179 = CARTESIAN_POINT('',(14.166666666667,-20.1,40.)); +#4179 = CARTESIAN_POINT('',(30.277777777778,17.8,40.)); #4180 = VECTOR('',#4181,1.); -#4181 = DIRECTION('',(1.,0.,-0.)); -#4182 = ORIENTED_EDGE('',*,*,#4130,.F.); -#4183 = PLANE('',#4184); -#4184 = AXIS2_PLACEMENT_3D('',#4185,#4186,#4187); -#4185 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4186 = DIRECTION('',(-0.,1.,0.)); +#4181 = DIRECTION('',(-0.,1.,0.)); +#4182 = ORIENTED_EDGE('',*,*,#3223,.T.); +#4183 = ORIENTED_EDGE('',*,*,#4145,.T.); +#4184 = PLANE('',#4185); +#4185 = AXIS2_PLACEMENT_3D('',#4186,#4187,#4188); +#4186 = CARTESIAN_POINT('',(-100.,18.,40.)); #4187 = DIRECTION('',(0.,0.,1.)); -#4188 = ADVANCED_FACE('',(#4189),#4205,.T.); -#4189 = FACE_BOUND('',#4190,.T.); -#4190 = EDGE_LOOP('',(#4191,#4197,#4198,#4204)); -#4191 = ORIENTED_EDGE('',*,*,#4192,.F.); -#4192 = EDGE_CURVE('',#4101,#4141,#4193,.T.); -#4193 = LINE('',#4194,#4195); -#4194 = CARTESIAN_POINT('',(14.166666666667,-17.9,8.)); -#4195 = VECTOR('',#4196,1.); -#4196 = DIRECTION('',(1.,0.,-0.)); -#4197 = ORIENTED_EDGE('',*,*,#4108,.T.); -#4198 = ORIENTED_EDGE('',*,*,#4199,.T.); -#4199 = EDGE_CURVE('',#4109,#4149,#4200,.T.); -#4200 = LINE('',#4201,#4202); -#4201 = CARTESIAN_POINT('',(14.166666666667,-17.9,40.)); -#4202 = VECTOR('',#4203,1.); -#4203 = DIRECTION('',(1.,0.,-0.)); -#4204 = ORIENTED_EDGE('',*,*,#4148,.F.); -#4205 = PLANE('',#4206); -#4206 = AXIS2_PLACEMENT_3D('',#4207,#4208,#4209); -#4207 = CARTESIAN_POINT('',(14.166666666667,-17.9,8.)); -#4208 = DIRECTION('',(-0.,1.,0.)); -#4209 = DIRECTION('',(0.,0.,1.)); -#4210 = ADVANCED_FACE('',(#4211),#4217,.F.); -#4211 = FACE_BOUND('',#4212,.F.); -#4212 = EDGE_LOOP('',(#4213,#4214,#4215,#4216)); -#4213 = ORIENTED_EDGE('',*,*,#4100,.F.); -#4214 = ORIENTED_EDGE('',*,*,#4170,.T.); -#4215 = ORIENTED_EDGE('',*,*,#4140,.T.); -#4216 = ORIENTED_EDGE('',*,*,#4192,.F.); -#4217 = PLANE('',#4218); -#4218 = AXIS2_PLACEMENT_3D('',#4219,#4220,#4221); -#4219 = CARTESIAN_POINT('',(14.166666666667,-20.1,8.)); -#4220 = DIRECTION('',(0.,0.,1.)); -#4221 = DIRECTION('',(1.,0.,-0.)); -#4222 = ADVANCED_FACE('',(#4223),#4229,.T.); -#4223 = FACE_BOUND('',#4224,.T.); -#4224 = EDGE_LOOP('',(#4225,#4226,#4227,#4228)); -#4225 = ORIENTED_EDGE('',*,*,#4116,.F.); -#4226 = ORIENTED_EDGE('',*,*,#4177,.T.); -#4227 = ORIENTED_EDGE('',*,*,#4156,.T.); -#4228 = ORIENTED_EDGE('',*,*,#4199,.F.); -#4229 = PLANE('',#4230); -#4230 = AXIS2_PLACEMENT_3D('',#4231,#4232,#4233); -#4231 = CARTESIAN_POINT('',(14.166666666667,-20.1,40.)); -#4232 = DIRECTION('',(0.,0.,1.)); -#4233 = DIRECTION('',(1.,0.,-0.)); -#4234 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4238)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#4235,#4236,#4237)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#4235 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#4236 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#4237 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#4238 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4235, - 'distance_accuracy_value','confusion accuracy'); -#4239 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4240,#4242); -#4240 = ( REPRESENTATION_RELATIONSHIP('','',#4083,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4241) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#4241 = ITEM_DEFINED_TRANSFORMATION('','',#11,#107); -#4242 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #4243); -#4243 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('24','WireDuct_LeftCombSlot_11', - '',#5,#4078,$); -#4244 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4080)); -#4245 = SHAPE_DEFINITION_REPRESENTATION(#4246,#4252); -#4246 = PRODUCT_DEFINITION_SHAPE('','',#4247); -#4247 = PRODUCT_DEFINITION('design','',#4248,#4251); -#4248 = PRODUCT_DEFINITION_FORMATION('','',#4249); -#4249 = PRODUCT('WireDuct_RightCombSlot_11','WireDuct_RightCombSlot_11', - '',(#4250)); -#4250 = PRODUCT_CONTEXT('',#2,'mechanical'); -#4251 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#4252 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4253),#4403); -#4253 = MANIFOLD_SOLID_BREP('',#4254); -#4254 = CLOSED_SHELL('',(#4255,#4295,#4335,#4357,#4379,#4391)); -#4255 = ADVANCED_FACE('',(#4256),#4290,.F.); -#4256 = FACE_BOUND('',#4257,.F.); -#4257 = EDGE_LOOP('',(#4258,#4268,#4276,#4284)); -#4258 = ORIENTED_EDGE('',*,*,#4259,.F.); -#4259 = EDGE_CURVE('',#4260,#4262,#4264,.T.); -#4260 = VERTEX_POINT('',#4261); -#4261 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); -#4262 = VERTEX_POINT('',#4263); -#4263 = CARTESIAN_POINT('',(14.166666666667,17.9,40.)); -#4264 = LINE('',#4265,#4266); -#4265 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); -#4266 = VECTOR('',#4267,1.); -#4267 = DIRECTION('',(0.,0.,1.)); -#4268 = ORIENTED_EDGE('',*,*,#4269,.T.); -#4269 = EDGE_CURVE('',#4260,#4270,#4272,.T.); -#4270 = VERTEX_POINT('',#4271); -#4271 = CARTESIAN_POINT('',(14.166666666667,20.1,8.)); -#4272 = LINE('',#4273,#4274); -#4273 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); -#4274 = VECTOR('',#4275,1.); -#4275 = DIRECTION('',(-0.,1.,0.)); -#4276 = ORIENTED_EDGE('',*,*,#4277,.T.); -#4277 = EDGE_CURVE('',#4270,#4278,#4280,.T.); -#4278 = VERTEX_POINT('',#4279); -#4279 = CARTESIAN_POINT('',(14.166666666667,20.1,40.)); -#4280 = LINE('',#4281,#4282); -#4281 = CARTESIAN_POINT('',(14.166666666667,20.1,8.)); -#4282 = VECTOR('',#4283,1.); +#4188 = DIRECTION('',(1.,0.,-0.)); +#4189 = ADVANCED_FACE('',(#4190),#4208,.F.); +#4190 = FACE_BOUND('',#4191,.F.); +#4191 = EDGE_LOOP('',(#4192,#4193,#4194,#4202)); +#4192 = ORIENTED_EDGE('',*,*,#4177,.F.); +#4193 = ORIENTED_EDGE('',*,*,#3215,.T.); +#4194 = ORIENTED_EDGE('',*,*,#4195,.T.); +#4195 = EDGE_CURVE('',#3208,#4196,#4198,.T.); +#4196 = VERTEX_POINT('',#4197); +#4197 = CARTESIAN_POINT('',(30.277777777778,20.,8.)); +#4198 = LINE('',#4199,#4200); +#4199 = CARTESIAN_POINT('',(30.277777777778,17.8,8.)); +#4200 = VECTOR('',#4201,1.); +#4201 = DIRECTION('',(-0.,1.,0.)); +#4202 = ORIENTED_EDGE('',*,*,#4203,.F.); +#4203 = EDGE_CURVE('',#4170,#4196,#4204,.T.); +#4204 = LINE('',#4205,#4206); +#4205 = CARTESIAN_POINT('',(30.277777777778,20.,4.)); +#4206 = VECTOR('',#4207,1.); +#4207 = DIRECTION('',(-0.,0.,-1.)); +#4208 = PLANE('',#4209); +#4209 = AXIS2_PLACEMENT_3D('',#4210,#4211,#4212); +#4210 = CARTESIAN_POINT('',(30.277777777778,17.8,8.)); +#4211 = DIRECTION('',(1.,0.,-0.)); +#4212 = DIRECTION('',(0.,0.,1.)); +#4213 = ADVANCED_FACE('',(#4214),#4232,.T.); +#4214 = FACE_BOUND('',#4215,.T.); +#4215 = EDGE_LOOP('',(#4216,#4224,#4225,#4226)); +#4216 = ORIENTED_EDGE('',*,*,#4217,.F.); +#4217 = EDGE_CURVE('',#3200,#4218,#4220,.T.); +#4218 = VERTEX_POINT('',#4219); +#4219 = CARTESIAN_POINT('',(25.277777777778,20.,8.)); +#4220 = LINE('',#4221,#4222); +#4221 = CARTESIAN_POINT('',(25.277777777778,17.8,8.)); +#4222 = VECTOR('',#4223,1.); +#4223 = DIRECTION('',(-0.,1.,0.)); +#4224 = ORIENTED_EDGE('',*,*,#3207,.T.); +#4225 = ORIENTED_EDGE('',*,*,#4195,.T.); +#4226 = ORIENTED_EDGE('',*,*,#4227,.F.); +#4227 = EDGE_CURVE('',#4218,#4196,#4228,.T.); +#4228 = LINE('',#4229,#4230); +#4229 = CARTESIAN_POINT('',(-37.36111111111,20.,8.)); +#4230 = VECTOR('',#4231,1.); +#4231 = DIRECTION('',(1.,0.,-0.)); +#4232 = PLANE('',#4233); +#4233 = AXIS2_PLACEMENT_3D('',#4234,#4235,#4236); +#4234 = CARTESIAN_POINT('',(25.277777777778,17.8,8.)); +#4235 = DIRECTION('',(0.,0.,1.)); +#4236 = DIRECTION('',(1.,0.,-0.)); +#4237 = ADVANCED_FACE('',(#4238),#4256,.T.); +#4238 = FACE_BOUND('',#4239,.T.); +#4239 = EDGE_LOOP('',(#4240,#4248,#4249,#4250)); +#4240 = ORIENTED_EDGE('',*,*,#4241,.F.); +#4241 = EDGE_CURVE('',#3192,#4242,#4244,.T.); +#4242 = VERTEX_POINT('',#4243); +#4243 = CARTESIAN_POINT('',(25.277777777778,20.,40.)); +#4244 = LINE('',#4245,#4246); +#4245 = CARTESIAN_POINT('',(25.277777777778,17.8,40.)); +#4246 = VECTOR('',#4247,1.); +#4247 = DIRECTION('',(-0.,1.,0.)); +#4248 = ORIENTED_EDGE('',*,*,#3199,.T.); +#4249 = ORIENTED_EDGE('',*,*,#4217,.T.); +#4250 = ORIENTED_EDGE('',*,*,#4251,.F.); +#4251 = EDGE_CURVE('',#4242,#4218,#4252,.T.); +#4252 = LINE('',#4253,#4254); +#4253 = CARTESIAN_POINT('',(25.277777777778,20.,4.)); +#4254 = VECTOR('',#4255,1.); +#4255 = DIRECTION('',(-0.,0.,-1.)); +#4256 = PLANE('',#4257); +#4257 = AXIS2_PLACEMENT_3D('',#4258,#4259,#4260); +#4258 = CARTESIAN_POINT('',(25.277777777778,17.8,8.)); +#4259 = DIRECTION('',(1.,0.,-0.)); +#4260 = DIRECTION('',(0.,0.,1.)); +#4261 = ADVANCED_FACE('',(#4262),#4280,.T.); +#4262 = FACE_BOUND('',#4263,.T.); +#4263 = EDGE_LOOP('',(#4264,#4272,#4278,#4279)); +#4264 = ORIENTED_EDGE('',*,*,#4265,.F.); +#4265 = EDGE_CURVE('',#4266,#4242,#4268,.T.); +#4266 = VERTEX_POINT('',#4267); +#4267 = CARTESIAN_POINT('',(19.166666666667,20.,40.)); +#4268 = LINE('',#4269,#4270); +#4269 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4270 = VECTOR('',#4271,1.); +#4271 = DIRECTION('',(1.,0.,-0.)); +#4272 = ORIENTED_EDGE('',*,*,#4273,.F.); +#4273 = EDGE_CURVE('',#3184,#4266,#4274,.T.); +#4274 = LINE('',#4275,#4276); +#4275 = CARTESIAN_POINT('',(19.166666666667,17.8,40.)); +#4276 = VECTOR('',#4277,1.); +#4277 = DIRECTION('',(-0.,1.,0.)); +#4278 = ORIENTED_EDGE('',*,*,#3191,.T.); +#4279 = ORIENTED_EDGE('',*,*,#4241,.T.); +#4280 = PLANE('',#4281); +#4281 = AXIS2_PLACEMENT_3D('',#4282,#4283,#4284); +#4282 = CARTESIAN_POINT('',(-100.,18.,40.)); #4283 = DIRECTION('',(0.,0.,1.)); -#4284 = ORIENTED_EDGE('',*,*,#4285,.F.); -#4285 = EDGE_CURVE('',#4262,#4278,#4286,.T.); -#4286 = LINE('',#4287,#4288); -#4287 = CARTESIAN_POINT('',(14.166666666667,17.9,40.)); -#4288 = VECTOR('',#4289,1.); -#4289 = DIRECTION('',(-0.,1.,0.)); -#4290 = PLANE('',#4291); -#4291 = AXIS2_PLACEMENT_3D('',#4292,#4293,#4294); -#4292 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); -#4293 = DIRECTION('',(1.,0.,-0.)); -#4294 = DIRECTION('',(0.,0.,1.)); -#4295 = ADVANCED_FACE('',(#4296),#4330,.T.); -#4296 = FACE_BOUND('',#4297,.T.); -#4297 = EDGE_LOOP('',(#4298,#4308,#4316,#4324)); +#4284 = DIRECTION('',(1.,0.,-0.)); +#4285 = ADVANCED_FACE('',(#4286),#4304,.F.); +#4286 = FACE_BOUND('',#4287,.F.); +#4287 = EDGE_LOOP('',(#4288,#4289,#4290,#4298)); +#4288 = ORIENTED_EDGE('',*,*,#4273,.F.); +#4289 = ORIENTED_EDGE('',*,*,#3183,.T.); +#4290 = ORIENTED_EDGE('',*,*,#4291,.T.); +#4291 = EDGE_CURVE('',#3176,#4292,#4294,.T.); +#4292 = VERTEX_POINT('',#4293); +#4293 = CARTESIAN_POINT('',(19.166666666667,20.,8.)); +#4294 = LINE('',#4295,#4296); +#4295 = CARTESIAN_POINT('',(19.166666666667,17.8,8.)); +#4296 = VECTOR('',#4297,1.); +#4297 = DIRECTION('',(-0.,1.,0.)); #4298 = ORIENTED_EDGE('',*,*,#4299,.F.); -#4299 = EDGE_CURVE('',#4300,#4302,#4304,.T.); -#4300 = VERTEX_POINT('',#4301); -#4301 = CARTESIAN_POINT('',(19.166666666667,17.9,8.)); -#4302 = VERTEX_POINT('',#4303); -#4303 = CARTESIAN_POINT('',(19.166666666667,17.9,40.)); -#4304 = LINE('',#4305,#4306); -#4305 = CARTESIAN_POINT('',(19.166666666667,17.9,8.)); -#4306 = VECTOR('',#4307,1.); -#4307 = DIRECTION('',(0.,0.,1.)); -#4308 = ORIENTED_EDGE('',*,*,#4309,.T.); -#4309 = EDGE_CURVE('',#4300,#4310,#4312,.T.); -#4310 = VERTEX_POINT('',#4311); -#4311 = CARTESIAN_POINT('',(19.166666666667,20.1,8.)); -#4312 = LINE('',#4313,#4314); -#4313 = CARTESIAN_POINT('',(19.166666666667,17.9,8.)); -#4314 = VECTOR('',#4315,1.); -#4315 = DIRECTION('',(-0.,1.,0.)); -#4316 = ORIENTED_EDGE('',*,*,#4317,.T.); -#4317 = EDGE_CURVE('',#4310,#4318,#4320,.T.); -#4318 = VERTEX_POINT('',#4319); -#4319 = CARTESIAN_POINT('',(19.166666666667,20.1,40.)); -#4320 = LINE('',#4321,#4322); -#4321 = CARTESIAN_POINT('',(19.166666666667,20.1,8.)); -#4322 = VECTOR('',#4323,1.); -#4323 = DIRECTION('',(0.,0.,1.)); -#4324 = ORIENTED_EDGE('',*,*,#4325,.F.); -#4325 = EDGE_CURVE('',#4302,#4318,#4326,.T.); -#4326 = LINE('',#4327,#4328); -#4327 = CARTESIAN_POINT('',(19.166666666667,17.9,40.)); -#4328 = VECTOR('',#4329,1.); -#4329 = DIRECTION('',(-0.,1.,0.)); -#4330 = PLANE('',#4331); -#4331 = AXIS2_PLACEMENT_3D('',#4332,#4333,#4334); -#4332 = CARTESIAN_POINT('',(19.166666666667,17.9,8.)); -#4333 = DIRECTION('',(1.,0.,-0.)); -#4334 = DIRECTION('',(0.,0.,1.)); -#4335 = ADVANCED_FACE('',(#4336),#4352,.F.); -#4336 = FACE_BOUND('',#4337,.F.); -#4337 = EDGE_LOOP('',(#4338,#4344,#4345,#4351)); -#4338 = ORIENTED_EDGE('',*,*,#4339,.F.); -#4339 = EDGE_CURVE('',#4260,#4300,#4340,.T.); +#4299 = EDGE_CURVE('',#4266,#4292,#4300,.T.); +#4300 = LINE('',#4301,#4302); +#4301 = CARTESIAN_POINT('',(19.166666666667,20.,4.)); +#4302 = VECTOR('',#4303,1.); +#4303 = DIRECTION('',(-0.,0.,-1.)); +#4304 = PLANE('',#4305); +#4305 = AXIS2_PLACEMENT_3D('',#4306,#4307,#4308); +#4306 = CARTESIAN_POINT('',(19.166666666667,17.8,8.)); +#4307 = DIRECTION('',(1.,0.,-0.)); +#4308 = DIRECTION('',(0.,0.,1.)); +#4309 = ADVANCED_FACE('',(#4310),#4328,.T.); +#4310 = FACE_BOUND('',#4311,.T.); +#4311 = EDGE_LOOP('',(#4312,#4320,#4321,#4322)); +#4312 = ORIENTED_EDGE('',*,*,#4313,.F.); +#4313 = EDGE_CURVE('',#3168,#4314,#4316,.T.); +#4314 = VERTEX_POINT('',#4315); +#4315 = CARTESIAN_POINT('',(14.166666666667,20.,8.)); +#4316 = LINE('',#4317,#4318); +#4317 = CARTESIAN_POINT('',(14.166666666667,17.8,8.)); +#4318 = VECTOR('',#4319,1.); +#4319 = DIRECTION('',(-0.,1.,0.)); +#4320 = ORIENTED_EDGE('',*,*,#3175,.T.); +#4321 = ORIENTED_EDGE('',*,*,#4291,.T.); +#4322 = ORIENTED_EDGE('',*,*,#4323,.F.); +#4323 = EDGE_CURVE('',#4314,#4292,#4324,.T.); +#4324 = LINE('',#4325,#4326); +#4325 = CARTESIAN_POINT('',(-42.91666666666,20.,8.)); +#4326 = VECTOR('',#4327,1.); +#4327 = DIRECTION('',(1.,0.,-0.)); +#4328 = PLANE('',#4329); +#4329 = AXIS2_PLACEMENT_3D('',#4330,#4331,#4332); +#4330 = CARTESIAN_POINT('',(14.166666666667,17.8,8.)); +#4331 = DIRECTION('',(0.,0.,1.)); +#4332 = DIRECTION('',(1.,0.,-0.)); +#4333 = ADVANCED_FACE('',(#4334),#4352,.T.); +#4334 = FACE_BOUND('',#4335,.T.); +#4335 = EDGE_LOOP('',(#4336,#4344,#4345,#4346)); +#4336 = ORIENTED_EDGE('',*,*,#4337,.F.); +#4337 = EDGE_CURVE('',#3160,#4338,#4340,.T.); +#4338 = VERTEX_POINT('',#4339); +#4339 = CARTESIAN_POINT('',(14.166666666667,20.,40.)); #4340 = LINE('',#4341,#4342); -#4341 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); +#4341 = CARTESIAN_POINT('',(14.166666666667,17.8,40.)); #4342 = VECTOR('',#4343,1.); -#4343 = DIRECTION('',(1.,0.,-0.)); -#4344 = ORIENTED_EDGE('',*,*,#4259,.T.); -#4345 = ORIENTED_EDGE('',*,*,#4346,.T.); -#4346 = EDGE_CURVE('',#4262,#4302,#4347,.T.); -#4347 = LINE('',#4348,#4349); -#4348 = CARTESIAN_POINT('',(14.166666666667,17.9,40.)); -#4349 = VECTOR('',#4350,1.); -#4350 = DIRECTION('',(1.,0.,-0.)); -#4351 = ORIENTED_EDGE('',*,*,#4299,.F.); +#4343 = DIRECTION('',(-0.,1.,0.)); +#4344 = ORIENTED_EDGE('',*,*,#3167,.T.); +#4345 = ORIENTED_EDGE('',*,*,#4313,.T.); +#4346 = ORIENTED_EDGE('',*,*,#4347,.F.); +#4347 = EDGE_CURVE('',#4338,#4314,#4348,.T.); +#4348 = LINE('',#4349,#4350); +#4349 = CARTESIAN_POINT('',(14.166666666667,20.,4.)); +#4350 = VECTOR('',#4351,1.); +#4351 = DIRECTION('',(-0.,0.,-1.)); #4352 = PLANE('',#4353); #4353 = AXIS2_PLACEMENT_3D('',#4354,#4355,#4356); -#4354 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); -#4355 = DIRECTION('',(-0.,1.,0.)); +#4354 = CARTESIAN_POINT('',(14.166666666667,17.8,8.)); +#4355 = DIRECTION('',(1.,0.,-0.)); #4356 = DIRECTION('',(0.,0.,1.)); -#4357 = ADVANCED_FACE('',(#4358),#4374,.T.); +#4357 = ADVANCED_FACE('',(#4358),#4376,.T.); #4358 = FACE_BOUND('',#4359,.T.); -#4359 = EDGE_LOOP('',(#4360,#4366,#4367,#4373)); +#4359 = EDGE_LOOP('',(#4360,#4368,#4374,#4375)); #4360 = ORIENTED_EDGE('',*,*,#4361,.F.); -#4361 = EDGE_CURVE('',#4270,#4310,#4362,.T.); -#4362 = LINE('',#4363,#4364); -#4363 = CARTESIAN_POINT('',(14.166666666667,20.1,8.)); -#4364 = VECTOR('',#4365,1.); -#4365 = DIRECTION('',(1.,0.,-0.)); -#4366 = ORIENTED_EDGE('',*,*,#4277,.T.); -#4367 = ORIENTED_EDGE('',*,*,#4368,.T.); -#4368 = EDGE_CURVE('',#4278,#4318,#4369,.T.); -#4369 = LINE('',#4370,#4371); -#4370 = CARTESIAN_POINT('',(14.166666666667,20.1,40.)); -#4371 = VECTOR('',#4372,1.); -#4372 = DIRECTION('',(1.,0.,-0.)); -#4373 = ORIENTED_EDGE('',*,*,#4317,.F.); -#4374 = PLANE('',#4375); -#4375 = AXIS2_PLACEMENT_3D('',#4376,#4377,#4378); -#4376 = CARTESIAN_POINT('',(14.166666666667,20.1,8.)); -#4377 = DIRECTION('',(-0.,1.,0.)); -#4378 = DIRECTION('',(0.,0.,1.)); -#4379 = ADVANCED_FACE('',(#4380),#4386,.F.); -#4380 = FACE_BOUND('',#4381,.F.); -#4381 = EDGE_LOOP('',(#4382,#4383,#4384,#4385)); -#4382 = ORIENTED_EDGE('',*,*,#4269,.F.); -#4383 = ORIENTED_EDGE('',*,*,#4339,.T.); -#4384 = ORIENTED_EDGE('',*,*,#4309,.T.); -#4385 = ORIENTED_EDGE('',*,*,#4361,.F.); -#4386 = PLANE('',#4387); -#4387 = AXIS2_PLACEMENT_3D('',#4388,#4389,#4390); -#4388 = CARTESIAN_POINT('',(14.166666666667,17.9,8.)); -#4389 = DIRECTION('',(0.,0.,1.)); -#4390 = DIRECTION('',(1.,0.,-0.)); -#4391 = ADVANCED_FACE('',(#4392),#4398,.T.); -#4392 = FACE_BOUND('',#4393,.T.); -#4393 = EDGE_LOOP('',(#4394,#4395,#4396,#4397)); -#4394 = ORIENTED_EDGE('',*,*,#4285,.F.); -#4395 = ORIENTED_EDGE('',*,*,#4346,.T.); -#4396 = ORIENTED_EDGE('',*,*,#4325,.T.); -#4397 = ORIENTED_EDGE('',*,*,#4368,.F.); -#4398 = PLANE('',#4399); -#4399 = AXIS2_PLACEMENT_3D('',#4400,#4401,#4402); -#4400 = CARTESIAN_POINT('',(14.166666666667,17.9,40.)); -#4401 = DIRECTION('',(0.,0.,1.)); -#4402 = DIRECTION('',(1.,0.,-0.)); -#4403 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4407)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#4404,#4405,#4406)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#4404 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#4405 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#4406 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#4407 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4404, - 'distance_accuracy_value','confusion accuracy'); -#4408 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4409,#4411); -#4409 = ( REPRESENTATION_RELATIONSHIP('','',#4252,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4410) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#4410 = ITEM_DEFINED_TRANSFORMATION('','',#11,#111); -#4411 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #4412); -#4412 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('25','WireDuct_RightCombSlot_11', - '',#5,#4247,$); -#4413 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4249)); -#4414 = SHAPE_DEFINITION_REPRESENTATION(#4415,#4421); -#4415 = PRODUCT_DEFINITION_SHAPE('','',#4416); -#4416 = PRODUCT_DEFINITION('design','',#4417,#4420); -#4417 = PRODUCT_DEFINITION_FORMATION('','',#4418); -#4418 = PRODUCT('WireDuct_LeftCombSlot_12','WireDuct_LeftCombSlot_12','' - ,(#4419)); -#4419 = PRODUCT_CONTEXT('',#2,'mechanical'); -#4420 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#4421 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4422),#4572); -#4422 = MANIFOLD_SOLID_BREP('',#4423); -#4423 = CLOSED_SHELL('',(#4424,#4464,#4504,#4526,#4548,#4560)); -#4424 = ADVANCED_FACE('',(#4425),#4459,.F.); -#4425 = FACE_BOUND('',#4426,.F.); -#4426 = EDGE_LOOP('',(#4427,#4437,#4445,#4453)); -#4427 = ORIENTED_EDGE('',*,*,#4428,.F.); -#4428 = EDGE_CURVE('',#4429,#4431,#4433,.T.); -#4429 = VERTEX_POINT('',#4430); -#4430 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4431 = VERTEX_POINT('',#4432); -#4432 = CARTESIAN_POINT('',(25.277777777778,-20.1,40.)); -#4433 = LINE('',#4434,#4435); -#4434 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4435 = VECTOR('',#4436,1.); -#4436 = DIRECTION('',(0.,0.,1.)); -#4437 = ORIENTED_EDGE('',*,*,#4438,.T.); -#4438 = EDGE_CURVE('',#4429,#4439,#4441,.T.); -#4439 = VERTEX_POINT('',#4440); -#4440 = CARTESIAN_POINT('',(25.277777777778,-17.9,8.)); -#4441 = LINE('',#4442,#4443); -#4442 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4443 = VECTOR('',#4444,1.); -#4444 = DIRECTION('',(-0.,1.,0.)); -#4445 = ORIENTED_EDGE('',*,*,#4446,.T.); -#4446 = EDGE_CURVE('',#4439,#4447,#4449,.T.); -#4447 = VERTEX_POINT('',#4448); -#4448 = CARTESIAN_POINT('',(25.277777777778,-17.9,40.)); -#4449 = LINE('',#4450,#4451); -#4450 = CARTESIAN_POINT('',(25.277777777778,-17.9,8.)); -#4451 = VECTOR('',#4452,1.); +#4361 = EDGE_CURVE('',#4362,#4338,#4364,.T.); +#4362 = VERTEX_POINT('',#4363); +#4363 = CARTESIAN_POINT('',(8.055555555556,20.,40.)); +#4364 = LINE('',#4365,#4366); +#4365 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4366 = VECTOR('',#4367,1.); +#4367 = DIRECTION('',(1.,0.,-0.)); +#4368 = ORIENTED_EDGE('',*,*,#4369,.F.); +#4369 = EDGE_CURVE('',#3152,#4362,#4370,.T.); +#4370 = LINE('',#4371,#4372); +#4371 = CARTESIAN_POINT('',(8.055555555556,17.8,40.)); +#4372 = VECTOR('',#4373,1.); +#4373 = DIRECTION('',(-0.,1.,0.)); +#4374 = ORIENTED_EDGE('',*,*,#3159,.T.); +#4375 = ORIENTED_EDGE('',*,*,#4337,.T.); +#4376 = PLANE('',#4377); +#4377 = AXIS2_PLACEMENT_3D('',#4378,#4379,#4380); +#4378 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4379 = DIRECTION('',(0.,0.,1.)); +#4380 = DIRECTION('',(1.,0.,-0.)); +#4381 = ADVANCED_FACE('',(#4382),#4400,.F.); +#4382 = FACE_BOUND('',#4383,.F.); +#4383 = EDGE_LOOP('',(#4384,#4385,#4386,#4394)); +#4384 = ORIENTED_EDGE('',*,*,#4369,.F.); +#4385 = ORIENTED_EDGE('',*,*,#3151,.T.); +#4386 = ORIENTED_EDGE('',*,*,#4387,.T.); +#4387 = EDGE_CURVE('',#3144,#4388,#4390,.T.); +#4388 = VERTEX_POINT('',#4389); +#4389 = CARTESIAN_POINT('',(8.055555555556,20.,8.)); +#4390 = LINE('',#4391,#4392); +#4391 = CARTESIAN_POINT('',(8.055555555556,17.8,8.)); +#4392 = VECTOR('',#4393,1.); +#4393 = DIRECTION('',(-0.,1.,0.)); +#4394 = ORIENTED_EDGE('',*,*,#4395,.F.); +#4395 = EDGE_CURVE('',#4362,#4388,#4396,.T.); +#4396 = LINE('',#4397,#4398); +#4397 = CARTESIAN_POINT('',(8.055555555556,20.,4.)); +#4398 = VECTOR('',#4399,1.); +#4399 = DIRECTION('',(-0.,0.,-1.)); +#4400 = PLANE('',#4401); +#4401 = AXIS2_PLACEMENT_3D('',#4402,#4403,#4404); +#4402 = CARTESIAN_POINT('',(8.055555555556,17.8,8.)); +#4403 = DIRECTION('',(1.,0.,-0.)); +#4404 = DIRECTION('',(0.,0.,1.)); +#4405 = ADVANCED_FACE('',(#4406),#4424,.T.); +#4406 = FACE_BOUND('',#4407,.T.); +#4407 = EDGE_LOOP('',(#4408,#4416,#4417,#4418)); +#4408 = ORIENTED_EDGE('',*,*,#4409,.F.); +#4409 = EDGE_CURVE('',#3136,#4410,#4412,.T.); +#4410 = VERTEX_POINT('',#4411); +#4411 = CARTESIAN_POINT('',(3.055555555556,20.,8.)); +#4412 = LINE('',#4413,#4414); +#4413 = CARTESIAN_POINT('',(3.055555555556,17.8,8.)); +#4414 = VECTOR('',#4415,1.); +#4415 = DIRECTION('',(-0.,1.,0.)); +#4416 = ORIENTED_EDGE('',*,*,#3143,.T.); +#4417 = ORIENTED_EDGE('',*,*,#4387,.T.); +#4418 = ORIENTED_EDGE('',*,*,#4419,.F.); +#4419 = EDGE_CURVE('',#4410,#4388,#4420,.T.); +#4420 = LINE('',#4421,#4422); +#4421 = CARTESIAN_POINT('',(-48.47222222222,20.,8.)); +#4422 = VECTOR('',#4423,1.); +#4423 = DIRECTION('',(1.,0.,-0.)); +#4424 = PLANE('',#4425); +#4425 = AXIS2_PLACEMENT_3D('',#4426,#4427,#4428); +#4426 = CARTESIAN_POINT('',(3.055555555556,17.8,8.)); +#4427 = DIRECTION('',(0.,0.,1.)); +#4428 = DIRECTION('',(1.,0.,-0.)); +#4429 = ADVANCED_FACE('',(#4430),#4448,.T.); +#4430 = FACE_BOUND('',#4431,.T.); +#4431 = EDGE_LOOP('',(#4432,#4440,#4441,#4442)); +#4432 = ORIENTED_EDGE('',*,*,#4433,.F.); +#4433 = EDGE_CURVE('',#3128,#4434,#4436,.T.); +#4434 = VERTEX_POINT('',#4435); +#4435 = CARTESIAN_POINT('',(3.055555555556,20.,40.)); +#4436 = LINE('',#4437,#4438); +#4437 = CARTESIAN_POINT('',(3.055555555556,17.8,40.)); +#4438 = VECTOR('',#4439,1.); +#4439 = DIRECTION('',(-0.,1.,0.)); +#4440 = ORIENTED_EDGE('',*,*,#3135,.T.); +#4441 = ORIENTED_EDGE('',*,*,#4409,.T.); +#4442 = ORIENTED_EDGE('',*,*,#4443,.F.); +#4443 = EDGE_CURVE('',#4434,#4410,#4444,.T.); +#4444 = LINE('',#4445,#4446); +#4445 = CARTESIAN_POINT('',(3.055555555556,20.,4.)); +#4446 = VECTOR('',#4447,1.); +#4447 = DIRECTION('',(-0.,0.,-1.)); +#4448 = PLANE('',#4449); +#4449 = AXIS2_PLACEMENT_3D('',#4450,#4451,#4452); +#4450 = CARTESIAN_POINT('',(3.055555555556,17.8,8.)); +#4451 = DIRECTION('',(1.,0.,-0.)); #4452 = DIRECTION('',(0.,0.,1.)); -#4453 = ORIENTED_EDGE('',*,*,#4454,.F.); -#4454 = EDGE_CURVE('',#4431,#4447,#4455,.T.); -#4455 = LINE('',#4456,#4457); -#4456 = CARTESIAN_POINT('',(25.277777777778,-20.1,40.)); -#4457 = VECTOR('',#4458,1.); -#4458 = DIRECTION('',(-0.,1.,0.)); -#4459 = PLANE('',#4460); -#4460 = AXIS2_PLACEMENT_3D('',#4461,#4462,#4463); -#4461 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4462 = DIRECTION('',(1.,0.,-0.)); -#4463 = DIRECTION('',(0.,0.,1.)); -#4464 = ADVANCED_FACE('',(#4465),#4499,.T.); -#4465 = FACE_BOUND('',#4466,.T.); -#4466 = EDGE_LOOP('',(#4467,#4477,#4485,#4493)); -#4467 = ORIENTED_EDGE('',*,*,#4468,.F.); -#4468 = EDGE_CURVE('',#4469,#4471,#4473,.T.); -#4469 = VERTEX_POINT('',#4470); -#4470 = CARTESIAN_POINT('',(30.277777777778,-20.1,8.)); -#4471 = VERTEX_POINT('',#4472); -#4472 = CARTESIAN_POINT('',(30.277777777778,-20.1,40.)); -#4473 = LINE('',#4474,#4475); -#4474 = CARTESIAN_POINT('',(30.277777777778,-20.1,8.)); -#4475 = VECTOR('',#4476,1.); -#4476 = DIRECTION('',(0.,0.,1.)); -#4477 = ORIENTED_EDGE('',*,*,#4478,.T.); -#4478 = EDGE_CURVE('',#4469,#4479,#4481,.T.); -#4479 = VERTEX_POINT('',#4480); -#4480 = CARTESIAN_POINT('',(30.277777777778,-17.9,8.)); -#4481 = LINE('',#4482,#4483); -#4482 = CARTESIAN_POINT('',(30.277777777778,-20.1,8.)); -#4483 = VECTOR('',#4484,1.); -#4484 = DIRECTION('',(-0.,1.,0.)); -#4485 = ORIENTED_EDGE('',*,*,#4486,.T.); -#4486 = EDGE_CURVE('',#4479,#4487,#4489,.T.); -#4487 = VERTEX_POINT('',#4488); -#4488 = CARTESIAN_POINT('',(30.277777777778,-17.9,40.)); -#4489 = LINE('',#4490,#4491); -#4490 = CARTESIAN_POINT('',(30.277777777778,-17.9,8.)); -#4491 = VECTOR('',#4492,1.); -#4492 = DIRECTION('',(0.,0.,1.)); -#4493 = ORIENTED_EDGE('',*,*,#4494,.F.); -#4494 = EDGE_CURVE('',#4471,#4487,#4495,.T.); -#4495 = LINE('',#4496,#4497); -#4496 = CARTESIAN_POINT('',(30.277777777778,-20.1,40.)); -#4497 = VECTOR('',#4498,1.); -#4498 = DIRECTION('',(-0.,1.,0.)); -#4499 = PLANE('',#4500); -#4500 = AXIS2_PLACEMENT_3D('',#4501,#4502,#4503); -#4501 = CARTESIAN_POINT('',(30.277777777778,-20.1,8.)); -#4502 = DIRECTION('',(1.,0.,-0.)); -#4503 = DIRECTION('',(0.,0.,1.)); -#4504 = ADVANCED_FACE('',(#4505),#4521,.F.); -#4505 = FACE_BOUND('',#4506,.F.); -#4506 = EDGE_LOOP('',(#4507,#4513,#4514,#4520)); -#4507 = ORIENTED_EDGE('',*,*,#4508,.F.); -#4508 = EDGE_CURVE('',#4429,#4469,#4509,.T.); -#4509 = LINE('',#4510,#4511); -#4510 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4511 = VECTOR('',#4512,1.); -#4512 = DIRECTION('',(1.,0.,-0.)); -#4513 = ORIENTED_EDGE('',*,*,#4428,.T.); -#4514 = ORIENTED_EDGE('',*,*,#4515,.T.); -#4515 = EDGE_CURVE('',#4431,#4471,#4516,.T.); +#4453 = ADVANCED_FACE('',(#4454),#4472,.T.); +#4454 = FACE_BOUND('',#4455,.T.); +#4455 = EDGE_LOOP('',(#4456,#4464,#4470,#4471)); +#4456 = ORIENTED_EDGE('',*,*,#4457,.F.); +#4457 = EDGE_CURVE('',#4458,#4434,#4460,.T.); +#4458 = VERTEX_POINT('',#4459); +#4459 = CARTESIAN_POINT('',(-3.055555555556,20.,40.)); +#4460 = LINE('',#4461,#4462); +#4461 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4462 = VECTOR('',#4463,1.); +#4463 = DIRECTION('',(1.,0.,-0.)); +#4464 = ORIENTED_EDGE('',*,*,#4465,.F.); +#4465 = EDGE_CURVE('',#3120,#4458,#4466,.T.); +#4466 = LINE('',#4467,#4468); +#4467 = CARTESIAN_POINT('',(-3.055555555556,17.8,40.)); +#4468 = VECTOR('',#4469,1.); +#4469 = DIRECTION('',(-0.,1.,0.)); +#4470 = ORIENTED_EDGE('',*,*,#3127,.T.); +#4471 = ORIENTED_EDGE('',*,*,#4433,.T.); +#4472 = PLANE('',#4473); +#4473 = AXIS2_PLACEMENT_3D('',#4474,#4475,#4476); +#4474 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4475 = DIRECTION('',(0.,0.,1.)); +#4476 = DIRECTION('',(1.,0.,-0.)); +#4477 = ADVANCED_FACE('',(#4478),#4496,.F.); +#4478 = FACE_BOUND('',#4479,.F.); +#4479 = EDGE_LOOP('',(#4480,#4481,#4482,#4490)); +#4480 = ORIENTED_EDGE('',*,*,#4465,.F.); +#4481 = ORIENTED_EDGE('',*,*,#3119,.T.); +#4482 = ORIENTED_EDGE('',*,*,#4483,.T.); +#4483 = EDGE_CURVE('',#3112,#4484,#4486,.T.); +#4484 = VERTEX_POINT('',#4485); +#4485 = CARTESIAN_POINT('',(-3.055555555556,20.,8.)); +#4486 = LINE('',#4487,#4488); +#4487 = CARTESIAN_POINT('',(-3.055555555556,17.8,8.)); +#4488 = VECTOR('',#4489,1.); +#4489 = DIRECTION('',(-0.,1.,0.)); +#4490 = ORIENTED_EDGE('',*,*,#4491,.F.); +#4491 = EDGE_CURVE('',#4458,#4484,#4492,.T.); +#4492 = LINE('',#4493,#4494); +#4493 = CARTESIAN_POINT('',(-3.055555555556,20.,4.)); +#4494 = VECTOR('',#4495,1.); +#4495 = DIRECTION('',(-0.,0.,-1.)); +#4496 = PLANE('',#4497); +#4497 = AXIS2_PLACEMENT_3D('',#4498,#4499,#4500); +#4498 = CARTESIAN_POINT('',(-3.055555555556,17.8,8.)); +#4499 = DIRECTION('',(1.,0.,-0.)); +#4500 = DIRECTION('',(0.,0.,1.)); +#4501 = ADVANCED_FACE('',(#4502),#4520,.T.); +#4502 = FACE_BOUND('',#4503,.T.); +#4503 = EDGE_LOOP('',(#4504,#4512,#4513,#4514)); +#4504 = ORIENTED_EDGE('',*,*,#4505,.F.); +#4505 = EDGE_CURVE('',#3104,#4506,#4508,.T.); +#4506 = VERTEX_POINT('',#4507); +#4507 = CARTESIAN_POINT('',(-8.055555555556,20.,8.)); +#4508 = LINE('',#4509,#4510); +#4509 = CARTESIAN_POINT('',(-8.055555555556,17.8,8.)); +#4510 = VECTOR('',#4511,1.); +#4511 = DIRECTION('',(-0.,1.,0.)); +#4512 = ORIENTED_EDGE('',*,*,#3111,.T.); +#4513 = ORIENTED_EDGE('',*,*,#4483,.T.); +#4514 = ORIENTED_EDGE('',*,*,#4515,.F.); +#4515 = EDGE_CURVE('',#4506,#4484,#4516,.T.); #4516 = LINE('',#4517,#4518); -#4517 = CARTESIAN_POINT('',(25.277777777778,-20.1,40.)); +#4517 = CARTESIAN_POINT('',(-54.02777777777,20.,8.)); #4518 = VECTOR('',#4519,1.); #4519 = DIRECTION('',(1.,0.,-0.)); -#4520 = ORIENTED_EDGE('',*,*,#4468,.F.); -#4521 = PLANE('',#4522); -#4522 = AXIS2_PLACEMENT_3D('',#4523,#4524,#4525); -#4523 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4524 = DIRECTION('',(-0.,1.,0.)); -#4525 = DIRECTION('',(0.,0.,1.)); -#4526 = ADVANCED_FACE('',(#4527),#4543,.T.); -#4527 = FACE_BOUND('',#4528,.T.); -#4528 = EDGE_LOOP('',(#4529,#4535,#4536,#4542)); -#4529 = ORIENTED_EDGE('',*,*,#4530,.F.); -#4530 = EDGE_CURVE('',#4439,#4479,#4531,.T.); -#4531 = LINE('',#4532,#4533); -#4532 = CARTESIAN_POINT('',(25.277777777778,-17.9,8.)); -#4533 = VECTOR('',#4534,1.); -#4534 = DIRECTION('',(1.,0.,-0.)); -#4535 = ORIENTED_EDGE('',*,*,#4446,.T.); -#4536 = ORIENTED_EDGE('',*,*,#4537,.T.); -#4537 = EDGE_CURVE('',#4447,#4487,#4538,.T.); -#4538 = LINE('',#4539,#4540); -#4539 = CARTESIAN_POINT('',(25.277777777778,-17.9,40.)); -#4540 = VECTOR('',#4541,1.); -#4541 = DIRECTION('',(1.,0.,-0.)); -#4542 = ORIENTED_EDGE('',*,*,#4486,.F.); -#4543 = PLANE('',#4544); -#4544 = AXIS2_PLACEMENT_3D('',#4545,#4546,#4547); -#4545 = CARTESIAN_POINT('',(25.277777777778,-17.9,8.)); -#4546 = DIRECTION('',(-0.,1.,0.)); -#4547 = DIRECTION('',(0.,0.,1.)); -#4548 = ADVANCED_FACE('',(#4549),#4555,.F.); -#4549 = FACE_BOUND('',#4550,.F.); -#4550 = EDGE_LOOP('',(#4551,#4552,#4553,#4554)); -#4551 = ORIENTED_EDGE('',*,*,#4438,.F.); -#4552 = ORIENTED_EDGE('',*,*,#4508,.T.); -#4553 = ORIENTED_EDGE('',*,*,#4478,.T.); -#4554 = ORIENTED_EDGE('',*,*,#4530,.F.); -#4555 = PLANE('',#4556); -#4556 = AXIS2_PLACEMENT_3D('',#4557,#4558,#4559); -#4557 = CARTESIAN_POINT('',(25.277777777778,-20.1,8.)); -#4558 = DIRECTION('',(0.,0.,1.)); +#4520 = PLANE('',#4521); +#4521 = AXIS2_PLACEMENT_3D('',#4522,#4523,#4524); +#4522 = CARTESIAN_POINT('',(-8.055555555556,17.8,8.)); +#4523 = DIRECTION('',(0.,0.,1.)); +#4524 = DIRECTION('',(1.,0.,-0.)); +#4525 = ADVANCED_FACE('',(#4526),#4544,.T.); +#4526 = FACE_BOUND('',#4527,.T.); +#4527 = EDGE_LOOP('',(#4528,#4536,#4537,#4538)); +#4528 = ORIENTED_EDGE('',*,*,#4529,.F.); +#4529 = EDGE_CURVE('',#3096,#4530,#4532,.T.); +#4530 = VERTEX_POINT('',#4531); +#4531 = CARTESIAN_POINT('',(-8.055555555556,20.,40.)); +#4532 = LINE('',#4533,#4534); +#4533 = CARTESIAN_POINT('',(-8.055555555556,17.8,40.)); +#4534 = VECTOR('',#4535,1.); +#4535 = DIRECTION('',(-0.,1.,0.)); +#4536 = ORIENTED_EDGE('',*,*,#3103,.T.); +#4537 = ORIENTED_EDGE('',*,*,#4505,.T.); +#4538 = ORIENTED_EDGE('',*,*,#4539,.F.); +#4539 = EDGE_CURVE('',#4530,#4506,#4540,.T.); +#4540 = LINE('',#4541,#4542); +#4541 = CARTESIAN_POINT('',(-8.055555555556,20.,4.)); +#4542 = VECTOR('',#4543,1.); +#4543 = DIRECTION('',(-0.,0.,-1.)); +#4544 = PLANE('',#4545); +#4545 = AXIS2_PLACEMENT_3D('',#4546,#4547,#4548); +#4546 = CARTESIAN_POINT('',(-8.055555555556,17.8,8.)); +#4547 = DIRECTION('',(1.,0.,-0.)); +#4548 = DIRECTION('',(0.,0.,1.)); +#4549 = ADVANCED_FACE('',(#4550),#4568,.T.); +#4550 = FACE_BOUND('',#4551,.T.); +#4551 = EDGE_LOOP('',(#4552,#4560,#4566,#4567)); +#4552 = ORIENTED_EDGE('',*,*,#4553,.F.); +#4553 = EDGE_CURVE('',#4554,#4530,#4556,.T.); +#4554 = VERTEX_POINT('',#4555); +#4555 = CARTESIAN_POINT('',(-14.16666666666,20.,40.)); +#4556 = LINE('',#4557,#4558); +#4557 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4558 = VECTOR('',#4559,1.); #4559 = DIRECTION('',(1.,0.,-0.)); -#4560 = ADVANCED_FACE('',(#4561),#4567,.T.); -#4561 = FACE_BOUND('',#4562,.T.); -#4562 = EDGE_LOOP('',(#4563,#4564,#4565,#4566)); -#4563 = ORIENTED_EDGE('',*,*,#4454,.F.); -#4564 = ORIENTED_EDGE('',*,*,#4515,.T.); -#4565 = ORIENTED_EDGE('',*,*,#4494,.T.); -#4566 = ORIENTED_EDGE('',*,*,#4537,.F.); -#4567 = PLANE('',#4568); -#4568 = AXIS2_PLACEMENT_3D('',#4569,#4570,#4571); -#4569 = CARTESIAN_POINT('',(25.277777777778,-20.1,40.)); -#4570 = DIRECTION('',(0.,0.,1.)); -#4571 = DIRECTION('',(1.,0.,-0.)); -#4572 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4576)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#4573,#4574,#4575)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#4573 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#4574 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#4575 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#4576 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4573, - 'distance_accuracy_value','confusion accuracy'); -#4577 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4578,#4580); -#4578 = ( REPRESENTATION_RELATIONSHIP('','',#4421,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4579) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#4579 = ITEM_DEFINED_TRANSFORMATION('','',#11,#115); -#4580 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #4581); -#4581 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('26','WireDuct_LeftCombSlot_12', - '',#5,#4416,$); -#4582 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4418)); -#4583 = SHAPE_DEFINITION_REPRESENTATION(#4584,#4590); -#4584 = PRODUCT_DEFINITION_SHAPE('','',#4585); -#4585 = PRODUCT_DEFINITION('design','',#4586,#4589); -#4586 = PRODUCT_DEFINITION_FORMATION('','',#4587); -#4587 = PRODUCT('WireDuct_RightCombSlot_12','WireDuct_RightCombSlot_12', - '',(#4588)); -#4588 = PRODUCT_CONTEXT('',#2,'mechanical'); -#4589 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#4590 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4591),#4741); -#4591 = MANIFOLD_SOLID_BREP('',#4592); -#4592 = CLOSED_SHELL('',(#4593,#4633,#4673,#4695,#4717,#4729)); -#4593 = ADVANCED_FACE('',(#4594),#4628,.F.); -#4594 = FACE_BOUND('',#4595,.F.); -#4595 = EDGE_LOOP('',(#4596,#4606,#4614,#4622)); -#4596 = ORIENTED_EDGE('',*,*,#4597,.F.); -#4597 = EDGE_CURVE('',#4598,#4600,#4602,.T.); -#4598 = VERTEX_POINT('',#4599); -#4599 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); -#4600 = VERTEX_POINT('',#4601); -#4601 = CARTESIAN_POINT('',(25.277777777778,17.9,40.)); -#4602 = LINE('',#4603,#4604); -#4603 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); -#4604 = VECTOR('',#4605,1.); -#4605 = DIRECTION('',(0.,0.,1.)); -#4606 = ORIENTED_EDGE('',*,*,#4607,.T.); -#4607 = EDGE_CURVE('',#4598,#4608,#4610,.T.); -#4608 = VERTEX_POINT('',#4609); -#4609 = CARTESIAN_POINT('',(25.277777777778,20.1,8.)); -#4610 = LINE('',#4611,#4612); -#4611 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); -#4612 = VECTOR('',#4613,1.); -#4613 = DIRECTION('',(-0.,1.,0.)); -#4614 = ORIENTED_EDGE('',*,*,#4615,.T.); -#4615 = EDGE_CURVE('',#4608,#4616,#4618,.T.); -#4616 = VERTEX_POINT('',#4617); -#4617 = CARTESIAN_POINT('',(25.277777777778,20.1,40.)); -#4618 = LINE('',#4619,#4620); -#4619 = CARTESIAN_POINT('',(25.277777777778,20.1,8.)); -#4620 = VECTOR('',#4621,1.); -#4621 = DIRECTION('',(0.,0.,1.)); -#4622 = ORIENTED_EDGE('',*,*,#4623,.F.); -#4623 = EDGE_CURVE('',#4600,#4616,#4624,.T.); -#4624 = LINE('',#4625,#4626); -#4625 = CARTESIAN_POINT('',(25.277777777778,17.9,40.)); -#4626 = VECTOR('',#4627,1.); -#4627 = DIRECTION('',(-0.,1.,0.)); -#4628 = PLANE('',#4629); -#4629 = AXIS2_PLACEMENT_3D('',#4630,#4631,#4632); -#4630 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); -#4631 = DIRECTION('',(1.,0.,-0.)); -#4632 = DIRECTION('',(0.,0.,1.)); -#4633 = ADVANCED_FACE('',(#4634),#4668,.T.); -#4634 = FACE_BOUND('',#4635,.T.); -#4635 = EDGE_LOOP('',(#4636,#4646,#4654,#4662)); -#4636 = ORIENTED_EDGE('',*,*,#4637,.F.); -#4637 = EDGE_CURVE('',#4638,#4640,#4642,.T.); -#4638 = VERTEX_POINT('',#4639); -#4639 = CARTESIAN_POINT('',(30.277777777778,17.9,8.)); -#4640 = VERTEX_POINT('',#4641); -#4641 = CARTESIAN_POINT('',(30.277777777778,17.9,40.)); -#4642 = LINE('',#4643,#4644); -#4643 = CARTESIAN_POINT('',(30.277777777778,17.9,8.)); -#4644 = VECTOR('',#4645,1.); -#4645 = DIRECTION('',(0.,0.,1.)); -#4646 = ORIENTED_EDGE('',*,*,#4647,.T.); -#4647 = EDGE_CURVE('',#4638,#4648,#4650,.T.); -#4648 = VERTEX_POINT('',#4649); -#4649 = CARTESIAN_POINT('',(30.277777777778,20.1,8.)); -#4650 = LINE('',#4651,#4652); -#4651 = CARTESIAN_POINT('',(30.277777777778,17.9,8.)); -#4652 = VECTOR('',#4653,1.); -#4653 = DIRECTION('',(-0.,1.,0.)); -#4654 = ORIENTED_EDGE('',*,*,#4655,.T.); -#4655 = EDGE_CURVE('',#4648,#4656,#4658,.T.); -#4656 = VERTEX_POINT('',#4657); -#4657 = CARTESIAN_POINT('',(30.277777777778,20.1,40.)); +#4560 = ORIENTED_EDGE('',*,*,#4561,.F.); +#4561 = EDGE_CURVE('',#3088,#4554,#4562,.T.); +#4562 = LINE('',#4563,#4564); +#4563 = CARTESIAN_POINT('',(-14.16666666666,17.8,40.)); +#4564 = VECTOR('',#4565,1.); +#4565 = DIRECTION('',(-0.,1.,0.)); +#4566 = ORIENTED_EDGE('',*,*,#3095,.T.); +#4567 = ORIENTED_EDGE('',*,*,#4529,.T.); +#4568 = PLANE('',#4569); +#4569 = AXIS2_PLACEMENT_3D('',#4570,#4571,#4572); +#4570 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4571 = DIRECTION('',(0.,0.,1.)); +#4572 = DIRECTION('',(1.,0.,-0.)); +#4573 = ADVANCED_FACE('',(#4574),#4592,.F.); +#4574 = FACE_BOUND('',#4575,.F.); +#4575 = EDGE_LOOP('',(#4576,#4577,#4578,#4586)); +#4576 = ORIENTED_EDGE('',*,*,#4561,.F.); +#4577 = ORIENTED_EDGE('',*,*,#3087,.T.); +#4578 = ORIENTED_EDGE('',*,*,#4579,.T.); +#4579 = EDGE_CURVE('',#3080,#4580,#4582,.T.); +#4580 = VERTEX_POINT('',#4581); +#4581 = CARTESIAN_POINT('',(-14.16666666666,20.,8.)); +#4582 = LINE('',#4583,#4584); +#4583 = CARTESIAN_POINT('',(-14.16666666666,17.8,8.)); +#4584 = VECTOR('',#4585,1.); +#4585 = DIRECTION('',(-0.,1.,0.)); +#4586 = ORIENTED_EDGE('',*,*,#4587,.F.); +#4587 = EDGE_CURVE('',#4554,#4580,#4588,.T.); +#4588 = LINE('',#4589,#4590); +#4589 = CARTESIAN_POINT('',(-14.16666666666,20.,4.)); +#4590 = VECTOR('',#4591,1.); +#4591 = DIRECTION('',(-0.,0.,-1.)); +#4592 = PLANE('',#4593); +#4593 = AXIS2_PLACEMENT_3D('',#4594,#4595,#4596); +#4594 = CARTESIAN_POINT('',(-14.16666666666,17.8,8.)); +#4595 = DIRECTION('',(1.,0.,-0.)); +#4596 = DIRECTION('',(0.,0.,1.)); +#4597 = ADVANCED_FACE('',(#4598),#4616,.T.); +#4598 = FACE_BOUND('',#4599,.T.); +#4599 = EDGE_LOOP('',(#4600,#4608,#4609,#4610)); +#4600 = ORIENTED_EDGE('',*,*,#4601,.F.); +#4601 = EDGE_CURVE('',#3072,#4602,#4604,.T.); +#4602 = VERTEX_POINT('',#4603); +#4603 = CARTESIAN_POINT('',(-19.16666666666,20.,8.)); +#4604 = LINE('',#4605,#4606); +#4605 = CARTESIAN_POINT('',(-19.16666666666,17.8,8.)); +#4606 = VECTOR('',#4607,1.); +#4607 = DIRECTION('',(-0.,1.,0.)); +#4608 = ORIENTED_EDGE('',*,*,#3079,.T.); +#4609 = ORIENTED_EDGE('',*,*,#4579,.T.); +#4610 = ORIENTED_EDGE('',*,*,#4611,.F.); +#4611 = EDGE_CURVE('',#4602,#4580,#4612,.T.); +#4612 = LINE('',#4613,#4614); +#4613 = CARTESIAN_POINT('',(-59.58333333333,20.,8.)); +#4614 = VECTOR('',#4615,1.); +#4615 = DIRECTION('',(1.,0.,-0.)); +#4616 = PLANE('',#4617); +#4617 = AXIS2_PLACEMENT_3D('',#4618,#4619,#4620); +#4618 = CARTESIAN_POINT('',(-19.16666666666,17.8,8.)); +#4619 = DIRECTION('',(0.,0.,1.)); +#4620 = DIRECTION('',(1.,0.,-0.)); +#4621 = ADVANCED_FACE('',(#4622),#4640,.T.); +#4622 = FACE_BOUND('',#4623,.T.); +#4623 = EDGE_LOOP('',(#4624,#4632,#4633,#4634)); +#4624 = ORIENTED_EDGE('',*,*,#4625,.F.); +#4625 = EDGE_CURVE('',#3064,#4626,#4628,.T.); +#4626 = VERTEX_POINT('',#4627); +#4627 = CARTESIAN_POINT('',(-19.16666666666,20.,40.)); +#4628 = LINE('',#4629,#4630); +#4629 = CARTESIAN_POINT('',(-19.16666666666,17.8,40.)); +#4630 = VECTOR('',#4631,1.); +#4631 = DIRECTION('',(-0.,1.,0.)); +#4632 = ORIENTED_EDGE('',*,*,#3071,.T.); +#4633 = ORIENTED_EDGE('',*,*,#4601,.T.); +#4634 = ORIENTED_EDGE('',*,*,#4635,.F.); +#4635 = EDGE_CURVE('',#4626,#4602,#4636,.T.); +#4636 = LINE('',#4637,#4638); +#4637 = CARTESIAN_POINT('',(-19.16666666666,20.,4.)); +#4638 = VECTOR('',#4639,1.); +#4639 = DIRECTION('',(-0.,0.,-1.)); +#4640 = PLANE('',#4641); +#4641 = AXIS2_PLACEMENT_3D('',#4642,#4643,#4644); +#4642 = CARTESIAN_POINT('',(-19.16666666666,17.8,8.)); +#4643 = DIRECTION('',(1.,0.,-0.)); +#4644 = DIRECTION('',(0.,0.,1.)); +#4645 = ADVANCED_FACE('',(#4646),#4664,.T.); +#4646 = FACE_BOUND('',#4647,.T.); +#4647 = EDGE_LOOP('',(#4648,#4656,#4662,#4663)); +#4648 = ORIENTED_EDGE('',*,*,#4649,.F.); +#4649 = EDGE_CURVE('',#4650,#4626,#4652,.T.); +#4650 = VERTEX_POINT('',#4651); +#4651 = CARTESIAN_POINT('',(-25.27777777777,20.,40.)); +#4652 = LINE('',#4653,#4654); +#4653 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4654 = VECTOR('',#4655,1.); +#4655 = DIRECTION('',(1.,0.,-0.)); +#4656 = ORIENTED_EDGE('',*,*,#4657,.F.); +#4657 = EDGE_CURVE('',#3056,#4650,#4658,.T.); #4658 = LINE('',#4659,#4660); -#4659 = CARTESIAN_POINT('',(30.277777777778,20.1,8.)); +#4659 = CARTESIAN_POINT('',(-25.27777777777,17.8,40.)); #4660 = VECTOR('',#4661,1.); -#4661 = DIRECTION('',(0.,0.,1.)); -#4662 = ORIENTED_EDGE('',*,*,#4663,.F.); -#4663 = EDGE_CURVE('',#4640,#4656,#4664,.T.); -#4664 = LINE('',#4665,#4666); -#4665 = CARTESIAN_POINT('',(30.277777777778,17.9,40.)); -#4666 = VECTOR('',#4667,1.); -#4667 = DIRECTION('',(-0.,1.,0.)); -#4668 = PLANE('',#4669); -#4669 = AXIS2_PLACEMENT_3D('',#4670,#4671,#4672); -#4670 = CARTESIAN_POINT('',(30.277777777778,17.9,8.)); -#4671 = DIRECTION('',(1.,0.,-0.)); -#4672 = DIRECTION('',(0.,0.,1.)); -#4673 = ADVANCED_FACE('',(#4674),#4690,.F.); -#4674 = FACE_BOUND('',#4675,.F.); -#4675 = EDGE_LOOP('',(#4676,#4682,#4683,#4689)); -#4676 = ORIENTED_EDGE('',*,*,#4677,.F.); -#4677 = EDGE_CURVE('',#4598,#4638,#4678,.T.); +#4661 = DIRECTION('',(-0.,1.,0.)); +#4662 = ORIENTED_EDGE('',*,*,#3063,.T.); +#4663 = ORIENTED_EDGE('',*,*,#4625,.T.); +#4664 = PLANE('',#4665); +#4665 = AXIS2_PLACEMENT_3D('',#4666,#4667,#4668); +#4666 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4667 = DIRECTION('',(0.,0.,1.)); +#4668 = DIRECTION('',(1.,0.,-0.)); +#4669 = ADVANCED_FACE('',(#4670),#4688,.F.); +#4670 = FACE_BOUND('',#4671,.F.); +#4671 = EDGE_LOOP('',(#4672,#4673,#4674,#4682)); +#4672 = ORIENTED_EDGE('',*,*,#4657,.F.); +#4673 = ORIENTED_EDGE('',*,*,#3055,.T.); +#4674 = ORIENTED_EDGE('',*,*,#4675,.T.); +#4675 = EDGE_CURVE('',#3048,#4676,#4678,.T.); +#4676 = VERTEX_POINT('',#4677); +#4677 = CARTESIAN_POINT('',(-25.27777777777,20.,8.)); #4678 = LINE('',#4679,#4680); -#4679 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); +#4679 = CARTESIAN_POINT('',(-25.27777777777,17.8,8.)); #4680 = VECTOR('',#4681,1.); -#4681 = DIRECTION('',(1.,0.,-0.)); -#4682 = ORIENTED_EDGE('',*,*,#4597,.T.); -#4683 = ORIENTED_EDGE('',*,*,#4684,.T.); -#4684 = EDGE_CURVE('',#4600,#4640,#4685,.T.); -#4685 = LINE('',#4686,#4687); -#4686 = CARTESIAN_POINT('',(25.277777777778,17.9,40.)); -#4687 = VECTOR('',#4688,1.); -#4688 = DIRECTION('',(1.,0.,-0.)); -#4689 = ORIENTED_EDGE('',*,*,#4637,.F.); -#4690 = PLANE('',#4691); -#4691 = AXIS2_PLACEMENT_3D('',#4692,#4693,#4694); -#4692 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); -#4693 = DIRECTION('',(-0.,1.,0.)); -#4694 = DIRECTION('',(0.,0.,1.)); -#4695 = ADVANCED_FACE('',(#4696),#4712,.T.); -#4696 = FACE_BOUND('',#4697,.T.); -#4697 = EDGE_LOOP('',(#4698,#4704,#4705,#4711)); -#4698 = ORIENTED_EDGE('',*,*,#4699,.F.); -#4699 = EDGE_CURVE('',#4608,#4648,#4700,.T.); +#4681 = DIRECTION('',(-0.,1.,0.)); +#4682 = ORIENTED_EDGE('',*,*,#4683,.F.); +#4683 = EDGE_CURVE('',#4650,#4676,#4684,.T.); +#4684 = LINE('',#4685,#4686); +#4685 = CARTESIAN_POINT('',(-25.27777777777,20.,4.)); +#4686 = VECTOR('',#4687,1.); +#4687 = DIRECTION('',(-0.,0.,-1.)); +#4688 = PLANE('',#4689); +#4689 = AXIS2_PLACEMENT_3D('',#4690,#4691,#4692); +#4690 = CARTESIAN_POINT('',(-25.27777777777,17.8,8.)); +#4691 = DIRECTION('',(1.,0.,-0.)); +#4692 = DIRECTION('',(0.,0.,1.)); +#4693 = ADVANCED_FACE('',(#4694),#4712,.T.); +#4694 = FACE_BOUND('',#4695,.T.); +#4695 = EDGE_LOOP('',(#4696,#4704,#4705,#4706)); +#4696 = ORIENTED_EDGE('',*,*,#4697,.F.); +#4697 = EDGE_CURVE('',#3040,#4698,#4700,.T.); +#4698 = VERTEX_POINT('',#4699); +#4699 = CARTESIAN_POINT('',(-30.27777777777,20.,8.)); #4700 = LINE('',#4701,#4702); -#4701 = CARTESIAN_POINT('',(25.277777777778,20.1,8.)); +#4701 = CARTESIAN_POINT('',(-30.27777777777,17.8,8.)); #4702 = VECTOR('',#4703,1.); -#4703 = DIRECTION('',(1.,0.,-0.)); -#4704 = ORIENTED_EDGE('',*,*,#4615,.T.); -#4705 = ORIENTED_EDGE('',*,*,#4706,.T.); -#4706 = EDGE_CURVE('',#4616,#4656,#4707,.T.); -#4707 = LINE('',#4708,#4709); -#4708 = CARTESIAN_POINT('',(25.277777777778,20.1,40.)); -#4709 = VECTOR('',#4710,1.); -#4710 = DIRECTION('',(1.,0.,-0.)); -#4711 = ORIENTED_EDGE('',*,*,#4655,.F.); +#4703 = DIRECTION('',(-0.,1.,0.)); +#4704 = ORIENTED_EDGE('',*,*,#3047,.T.); +#4705 = ORIENTED_EDGE('',*,*,#4675,.T.); +#4706 = ORIENTED_EDGE('',*,*,#4707,.F.); +#4707 = EDGE_CURVE('',#4698,#4676,#4708,.T.); +#4708 = LINE('',#4709,#4710); +#4709 = CARTESIAN_POINT('',(-65.13888888888,20.,8.)); +#4710 = VECTOR('',#4711,1.); +#4711 = DIRECTION('',(1.,0.,-0.)); #4712 = PLANE('',#4713); #4713 = AXIS2_PLACEMENT_3D('',#4714,#4715,#4716); -#4714 = CARTESIAN_POINT('',(25.277777777778,20.1,8.)); -#4715 = DIRECTION('',(-0.,1.,0.)); -#4716 = DIRECTION('',(0.,0.,1.)); -#4717 = ADVANCED_FACE('',(#4718),#4724,.F.); -#4718 = FACE_BOUND('',#4719,.F.); -#4719 = EDGE_LOOP('',(#4720,#4721,#4722,#4723)); -#4720 = ORIENTED_EDGE('',*,*,#4607,.F.); -#4721 = ORIENTED_EDGE('',*,*,#4677,.T.); -#4722 = ORIENTED_EDGE('',*,*,#4647,.T.); -#4723 = ORIENTED_EDGE('',*,*,#4699,.F.); -#4724 = PLANE('',#4725); -#4725 = AXIS2_PLACEMENT_3D('',#4726,#4727,#4728); -#4726 = CARTESIAN_POINT('',(25.277777777778,17.9,8.)); -#4727 = DIRECTION('',(0.,0.,1.)); -#4728 = DIRECTION('',(1.,0.,-0.)); -#4729 = ADVANCED_FACE('',(#4730),#4736,.T.); -#4730 = FACE_BOUND('',#4731,.T.); -#4731 = EDGE_LOOP('',(#4732,#4733,#4734,#4735)); -#4732 = ORIENTED_EDGE('',*,*,#4623,.F.); -#4733 = ORIENTED_EDGE('',*,*,#4684,.T.); -#4734 = ORIENTED_EDGE('',*,*,#4663,.T.); -#4735 = ORIENTED_EDGE('',*,*,#4706,.F.); +#4714 = CARTESIAN_POINT('',(-30.27777777777,17.8,8.)); +#4715 = DIRECTION('',(0.,0.,1.)); +#4716 = DIRECTION('',(1.,0.,-0.)); +#4717 = ADVANCED_FACE('',(#4718),#4736,.T.); +#4718 = FACE_BOUND('',#4719,.T.); +#4719 = EDGE_LOOP('',(#4720,#4728,#4729,#4730)); +#4720 = ORIENTED_EDGE('',*,*,#4721,.F.); +#4721 = EDGE_CURVE('',#3032,#4722,#4724,.T.); +#4722 = VERTEX_POINT('',#4723); +#4723 = CARTESIAN_POINT('',(-30.27777777777,20.,40.)); +#4724 = LINE('',#4725,#4726); +#4725 = CARTESIAN_POINT('',(-30.27777777777,17.8,40.)); +#4726 = VECTOR('',#4727,1.); +#4727 = DIRECTION('',(-0.,1.,0.)); +#4728 = ORIENTED_EDGE('',*,*,#3039,.T.); +#4729 = ORIENTED_EDGE('',*,*,#4697,.T.); +#4730 = ORIENTED_EDGE('',*,*,#4731,.F.); +#4731 = EDGE_CURVE('',#4722,#4698,#4732,.T.); +#4732 = LINE('',#4733,#4734); +#4733 = CARTESIAN_POINT('',(-30.27777777777,20.,4.)); +#4734 = VECTOR('',#4735,1.); +#4735 = DIRECTION('',(-0.,0.,-1.)); #4736 = PLANE('',#4737); #4737 = AXIS2_PLACEMENT_3D('',#4738,#4739,#4740); -#4738 = CARTESIAN_POINT('',(25.277777777778,17.9,40.)); -#4739 = DIRECTION('',(0.,0.,1.)); -#4740 = DIRECTION('',(1.,0.,-0.)); -#4741 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4745)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#4742,#4743,#4744)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#4742 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#4743 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#4744 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#4745 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4742, - 'distance_accuracy_value','confusion accuracy'); -#4746 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4747,#4749); -#4747 = ( REPRESENTATION_RELATIONSHIP('','',#4590,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4748) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#4748 = ITEM_DEFINED_TRANSFORMATION('','',#11,#119); -#4749 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #4750); -#4750 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('27','WireDuct_RightCombSlot_12', - '',#5,#4585,$); -#4751 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4587)); -#4752 = SHAPE_DEFINITION_REPRESENTATION(#4753,#4759); -#4753 = PRODUCT_DEFINITION_SHAPE('','',#4754); -#4754 = PRODUCT_DEFINITION('design','',#4755,#4758); -#4755 = PRODUCT_DEFINITION_FORMATION('','',#4756); -#4756 = PRODUCT('WireDuct_LeftCombSlot_13','WireDuct_LeftCombSlot_13','' - ,(#4757)); -#4757 = PRODUCT_CONTEXT('',#2,'mechanical'); -#4758 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#4759 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4760),#4910); -#4760 = MANIFOLD_SOLID_BREP('',#4761); -#4761 = CLOSED_SHELL('',(#4762,#4802,#4842,#4864,#4886,#4898)); -#4762 = ADVANCED_FACE('',(#4763),#4797,.F.); -#4763 = FACE_BOUND('',#4764,.F.); -#4764 = EDGE_LOOP('',(#4765,#4775,#4783,#4791)); -#4765 = ORIENTED_EDGE('',*,*,#4766,.F.); -#4766 = EDGE_CURVE('',#4767,#4769,#4771,.T.); -#4767 = VERTEX_POINT('',#4768); -#4768 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4769 = VERTEX_POINT('',#4770); -#4770 = CARTESIAN_POINT('',(36.388888888889,-20.1,40.)); -#4771 = LINE('',#4772,#4773); -#4772 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4773 = VECTOR('',#4774,1.); -#4774 = DIRECTION('',(0.,0.,1.)); -#4775 = ORIENTED_EDGE('',*,*,#4776,.T.); -#4776 = EDGE_CURVE('',#4767,#4777,#4779,.T.); -#4777 = VERTEX_POINT('',#4778); -#4778 = CARTESIAN_POINT('',(36.388888888889,-17.9,8.)); -#4779 = LINE('',#4780,#4781); -#4780 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4781 = VECTOR('',#4782,1.); -#4782 = DIRECTION('',(-0.,1.,0.)); -#4783 = ORIENTED_EDGE('',*,*,#4784,.T.); -#4784 = EDGE_CURVE('',#4777,#4785,#4787,.T.); -#4785 = VERTEX_POINT('',#4786); -#4786 = CARTESIAN_POINT('',(36.388888888889,-17.9,40.)); -#4787 = LINE('',#4788,#4789); -#4788 = CARTESIAN_POINT('',(36.388888888889,-17.9,8.)); -#4789 = VECTOR('',#4790,1.); -#4790 = DIRECTION('',(0.,0.,1.)); -#4791 = ORIENTED_EDGE('',*,*,#4792,.F.); -#4792 = EDGE_CURVE('',#4769,#4785,#4793,.T.); -#4793 = LINE('',#4794,#4795); -#4794 = CARTESIAN_POINT('',(36.388888888889,-20.1,40.)); -#4795 = VECTOR('',#4796,1.); -#4796 = DIRECTION('',(-0.,1.,0.)); -#4797 = PLANE('',#4798); -#4798 = AXIS2_PLACEMENT_3D('',#4799,#4800,#4801); -#4799 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4800 = DIRECTION('',(1.,0.,-0.)); -#4801 = DIRECTION('',(0.,0.,1.)); -#4802 = ADVANCED_FACE('',(#4803),#4837,.T.); -#4803 = FACE_BOUND('',#4804,.T.); -#4804 = EDGE_LOOP('',(#4805,#4815,#4823,#4831)); -#4805 = ORIENTED_EDGE('',*,*,#4806,.F.); -#4806 = EDGE_CURVE('',#4807,#4809,#4811,.T.); -#4807 = VERTEX_POINT('',#4808); -#4808 = CARTESIAN_POINT('',(41.388888888889,-20.1,8.)); -#4809 = VERTEX_POINT('',#4810); -#4810 = CARTESIAN_POINT('',(41.388888888889,-20.1,40.)); -#4811 = LINE('',#4812,#4813); -#4812 = CARTESIAN_POINT('',(41.388888888889,-20.1,8.)); -#4813 = VECTOR('',#4814,1.); -#4814 = DIRECTION('',(0.,0.,1.)); -#4815 = ORIENTED_EDGE('',*,*,#4816,.T.); -#4816 = EDGE_CURVE('',#4807,#4817,#4819,.T.); -#4817 = VERTEX_POINT('',#4818); -#4818 = CARTESIAN_POINT('',(41.388888888889,-17.9,8.)); -#4819 = LINE('',#4820,#4821); -#4820 = CARTESIAN_POINT('',(41.388888888889,-20.1,8.)); -#4821 = VECTOR('',#4822,1.); -#4822 = DIRECTION('',(-0.,1.,0.)); -#4823 = ORIENTED_EDGE('',*,*,#4824,.T.); -#4824 = EDGE_CURVE('',#4817,#4825,#4827,.T.); -#4825 = VERTEX_POINT('',#4826); -#4826 = CARTESIAN_POINT('',(41.388888888889,-17.9,40.)); -#4827 = LINE('',#4828,#4829); -#4828 = CARTESIAN_POINT('',(41.388888888889,-17.9,8.)); -#4829 = VECTOR('',#4830,1.); -#4830 = DIRECTION('',(0.,0.,1.)); -#4831 = ORIENTED_EDGE('',*,*,#4832,.F.); -#4832 = EDGE_CURVE('',#4809,#4825,#4833,.T.); -#4833 = LINE('',#4834,#4835); -#4834 = CARTESIAN_POINT('',(41.388888888889,-20.1,40.)); -#4835 = VECTOR('',#4836,1.); -#4836 = DIRECTION('',(-0.,1.,0.)); -#4837 = PLANE('',#4838); -#4838 = AXIS2_PLACEMENT_3D('',#4839,#4840,#4841); -#4839 = CARTESIAN_POINT('',(41.388888888889,-20.1,8.)); -#4840 = DIRECTION('',(1.,0.,-0.)); -#4841 = DIRECTION('',(0.,0.,1.)); -#4842 = ADVANCED_FACE('',(#4843),#4859,.F.); -#4843 = FACE_BOUND('',#4844,.F.); -#4844 = EDGE_LOOP('',(#4845,#4851,#4852,#4858)); -#4845 = ORIENTED_EDGE('',*,*,#4846,.F.); -#4846 = EDGE_CURVE('',#4767,#4807,#4847,.T.); -#4847 = LINE('',#4848,#4849); -#4848 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4849 = VECTOR('',#4850,1.); -#4850 = DIRECTION('',(1.,0.,-0.)); -#4851 = ORIENTED_EDGE('',*,*,#4766,.T.); -#4852 = ORIENTED_EDGE('',*,*,#4853,.T.); -#4853 = EDGE_CURVE('',#4769,#4809,#4854,.T.); -#4854 = LINE('',#4855,#4856); -#4855 = CARTESIAN_POINT('',(36.388888888889,-20.1,40.)); -#4856 = VECTOR('',#4857,1.); -#4857 = DIRECTION('',(1.,0.,-0.)); -#4858 = ORIENTED_EDGE('',*,*,#4806,.F.); -#4859 = PLANE('',#4860); -#4860 = AXIS2_PLACEMENT_3D('',#4861,#4862,#4863); -#4861 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4862 = DIRECTION('',(-0.,1.,0.)); -#4863 = DIRECTION('',(0.,0.,1.)); -#4864 = ADVANCED_FACE('',(#4865),#4881,.T.); -#4865 = FACE_BOUND('',#4866,.T.); -#4866 = EDGE_LOOP('',(#4867,#4873,#4874,#4880)); -#4867 = ORIENTED_EDGE('',*,*,#4868,.F.); -#4868 = EDGE_CURVE('',#4777,#4817,#4869,.T.); -#4869 = LINE('',#4870,#4871); -#4870 = CARTESIAN_POINT('',(36.388888888889,-17.9,8.)); -#4871 = VECTOR('',#4872,1.); -#4872 = DIRECTION('',(1.,0.,-0.)); -#4873 = ORIENTED_EDGE('',*,*,#4784,.T.); -#4874 = ORIENTED_EDGE('',*,*,#4875,.T.); -#4875 = EDGE_CURVE('',#4785,#4825,#4876,.T.); +#4738 = CARTESIAN_POINT('',(-30.27777777777,17.8,8.)); +#4739 = DIRECTION('',(1.,0.,-0.)); +#4740 = DIRECTION('',(0.,0.,1.)); +#4741 = ADVANCED_FACE('',(#4742),#4760,.T.); +#4742 = FACE_BOUND('',#4743,.T.); +#4743 = EDGE_LOOP('',(#4744,#4752,#4758,#4759)); +#4744 = ORIENTED_EDGE('',*,*,#4745,.F.); +#4745 = EDGE_CURVE('',#4746,#4722,#4748,.T.); +#4746 = VERTEX_POINT('',#4747); +#4747 = CARTESIAN_POINT('',(-36.38888888888,20.,40.)); +#4748 = LINE('',#4749,#4750); +#4749 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4750 = VECTOR('',#4751,1.); +#4751 = DIRECTION('',(1.,0.,-0.)); +#4752 = ORIENTED_EDGE('',*,*,#4753,.F.); +#4753 = EDGE_CURVE('',#3024,#4746,#4754,.T.); +#4754 = LINE('',#4755,#4756); +#4755 = CARTESIAN_POINT('',(-36.38888888888,17.8,40.)); +#4756 = VECTOR('',#4757,1.); +#4757 = DIRECTION('',(-0.,1.,0.)); +#4758 = ORIENTED_EDGE('',*,*,#3031,.T.); +#4759 = ORIENTED_EDGE('',*,*,#4721,.T.); +#4760 = PLANE('',#4761); +#4761 = AXIS2_PLACEMENT_3D('',#4762,#4763,#4764); +#4762 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4763 = DIRECTION('',(0.,0.,1.)); +#4764 = DIRECTION('',(1.,0.,-0.)); +#4765 = ADVANCED_FACE('',(#4766),#4784,.F.); +#4766 = FACE_BOUND('',#4767,.F.); +#4767 = EDGE_LOOP('',(#4768,#4769,#4770,#4778)); +#4768 = ORIENTED_EDGE('',*,*,#4753,.F.); +#4769 = ORIENTED_EDGE('',*,*,#3023,.T.); +#4770 = ORIENTED_EDGE('',*,*,#4771,.T.); +#4771 = EDGE_CURVE('',#3016,#4772,#4774,.T.); +#4772 = VERTEX_POINT('',#4773); +#4773 = CARTESIAN_POINT('',(-36.38888888888,20.,8.)); +#4774 = LINE('',#4775,#4776); +#4775 = CARTESIAN_POINT('',(-36.38888888888,17.8,8.)); +#4776 = VECTOR('',#4777,1.); +#4777 = DIRECTION('',(-0.,1.,0.)); +#4778 = ORIENTED_EDGE('',*,*,#4779,.F.); +#4779 = EDGE_CURVE('',#4746,#4772,#4780,.T.); +#4780 = LINE('',#4781,#4782); +#4781 = CARTESIAN_POINT('',(-36.38888888888,20.,4.)); +#4782 = VECTOR('',#4783,1.); +#4783 = DIRECTION('',(-0.,0.,-1.)); +#4784 = PLANE('',#4785); +#4785 = AXIS2_PLACEMENT_3D('',#4786,#4787,#4788); +#4786 = CARTESIAN_POINT('',(-36.38888888888,17.8,8.)); +#4787 = DIRECTION('',(1.,0.,-0.)); +#4788 = DIRECTION('',(0.,0.,1.)); +#4789 = ADVANCED_FACE('',(#4790),#4808,.T.); +#4790 = FACE_BOUND('',#4791,.T.); +#4791 = EDGE_LOOP('',(#4792,#4800,#4801,#4802)); +#4792 = ORIENTED_EDGE('',*,*,#4793,.F.); +#4793 = EDGE_CURVE('',#3008,#4794,#4796,.T.); +#4794 = VERTEX_POINT('',#4795); +#4795 = CARTESIAN_POINT('',(-41.38888888888,20.,8.)); +#4796 = LINE('',#4797,#4798); +#4797 = CARTESIAN_POINT('',(-41.38888888888,17.8,8.)); +#4798 = VECTOR('',#4799,1.); +#4799 = DIRECTION('',(-0.,1.,0.)); +#4800 = ORIENTED_EDGE('',*,*,#3015,.T.); +#4801 = ORIENTED_EDGE('',*,*,#4771,.T.); +#4802 = ORIENTED_EDGE('',*,*,#4803,.F.); +#4803 = EDGE_CURVE('',#4794,#4772,#4804,.T.); +#4804 = LINE('',#4805,#4806); +#4805 = CARTESIAN_POINT('',(-70.69444444444,20.,8.)); +#4806 = VECTOR('',#4807,1.); +#4807 = DIRECTION('',(1.,0.,-0.)); +#4808 = PLANE('',#4809); +#4809 = AXIS2_PLACEMENT_3D('',#4810,#4811,#4812); +#4810 = CARTESIAN_POINT('',(-41.38888888888,17.8,8.)); +#4811 = DIRECTION('',(0.,0.,1.)); +#4812 = DIRECTION('',(1.,0.,-0.)); +#4813 = ADVANCED_FACE('',(#4814),#4832,.T.); +#4814 = FACE_BOUND('',#4815,.T.); +#4815 = EDGE_LOOP('',(#4816,#4824,#4825,#4826)); +#4816 = ORIENTED_EDGE('',*,*,#4817,.F.); +#4817 = EDGE_CURVE('',#3000,#4818,#4820,.T.); +#4818 = VERTEX_POINT('',#4819); +#4819 = CARTESIAN_POINT('',(-41.38888888888,20.,40.)); +#4820 = LINE('',#4821,#4822); +#4821 = CARTESIAN_POINT('',(-41.38888888888,17.8,40.)); +#4822 = VECTOR('',#4823,1.); +#4823 = DIRECTION('',(-0.,1.,0.)); +#4824 = ORIENTED_EDGE('',*,*,#3007,.T.); +#4825 = ORIENTED_EDGE('',*,*,#4793,.T.); +#4826 = ORIENTED_EDGE('',*,*,#4827,.F.); +#4827 = EDGE_CURVE('',#4818,#4794,#4828,.T.); +#4828 = LINE('',#4829,#4830); +#4829 = CARTESIAN_POINT('',(-41.38888888888,20.,4.)); +#4830 = VECTOR('',#4831,1.); +#4831 = DIRECTION('',(-0.,0.,-1.)); +#4832 = PLANE('',#4833); +#4833 = AXIS2_PLACEMENT_3D('',#4834,#4835,#4836); +#4834 = CARTESIAN_POINT('',(-41.38888888888,17.8,8.)); +#4835 = DIRECTION('',(1.,0.,-0.)); +#4836 = DIRECTION('',(0.,0.,1.)); +#4837 = ADVANCED_FACE('',(#4838),#4856,.T.); +#4838 = FACE_BOUND('',#4839,.T.); +#4839 = EDGE_LOOP('',(#4840,#4848,#4854,#4855)); +#4840 = ORIENTED_EDGE('',*,*,#4841,.F.); +#4841 = EDGE_CURVE('',#4842,#4818,#4844,.T.); +#4842 = VERTEX_POINT('',#4843); +#4843 = CARTESIAN_POINT('',(-47.5,20.,40.)); +#4844 = LINE('',#4845,#4846); +#4845 = CARTESIAN_POINT('',(-100.,20.,40.)); +#4846 = VECTOR('',#4847,1.); +#4847 = DIRECTION('',(1.,0.,-0.)); +#4848 = ORIENTED_EDGE('',*,*,#4849,.F.); +#4849 = EDGE_CURVE('',#2992,#4842,#4850,.T.); +#4850 = LINE('',#4851,#4852); +#4851 = CARTESIAN_POINT('',(-47.5,17.8,40.)); +#4852 = VECTOR('',#4853,1.); +#4853 = DIRECTION('',(-0.,1.,0.)); +#4854 = ORIENTED_EDGE('',*,*,#2999,.T.); +#4855 = ORIENTED_EDGE('',*,*,#4817,.T.); +#4856 = PLANE('',#4857); +#4857 = AXIS2_PLACEMENT_3D('',#4858,#4859,#4860); +#4858 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4859 = DIRECTION('',(0.,0.,1.)); +#4860 = DIRECTION('',(1.,0.,-0.)); +#4861 = ADVANCED_FACE('',(#4862),#4880,.F.); +#4862 = FACE_BOUND('',#4863,.F.); +#4863 = EDGE_LOOP('',(#4864,#4865,#4866,#4874)); +#4864 = ORIENTED_EDGE('',*,*,#4849,.F.); +#4865 = ORIENTED_EDGE('',*,*,#2991,.T.); +#4866 = ORIENTED_EDGE('',*,*,#4867,.T.); +#4867 = EDGE_CURVE('',#2984,#4868,#4870,.T.); +#4868 = VERTEX_POINT('',#4869); +#4869 = CARTESIAN_POINT('',(-47.5,20.,8.)); +#4870 = LINE('',#4871,#4872); +#4871 = CARTESIAN_POINT('',(-47.5,17.8,8.)); +#4872 = VECTOR('',#4873,1.); +#4873 = DIRECTION('',(-0.,1.,0.)); +#4874 = ORIENTED_EDGE('',*,*,#4875,.F.); +#4875 = EDGE_CURVE('',#4842,#4868,#4876,.T.); #4876 = LINE('',#4877,#4878); -#4877 = CARTESIAN_POINT('',(36.388888888889,-17.9,40.)); +#4877 = CARTESIAN_POINT('',(-47.5,20.,4.)); #4878 = VECTOR('',#4879,1.); -#4879 = DIRECTION('',(1.,0.,-0.)); -#4880 = ORIENTED_EDGE('',*,*,#4824,.F.); -#4881 = PLANE('',#4882); -#4882 = AXIS2_PLACEMENT_3D('',#4883,#4884,#4885); -#4883 = CARTESIAN_POINT('',(36.388888888889,-17.9,8.)); -#4884 = DIRECTION('',(-0.,1.,0.)); -#4885 = DIRECTION('',(0.,0.,1.)); -#4886 = ADVANCED_FACE('',(#4887),#4893,.F.); -#4887 = FACE_BOUND('',#4888,.F.); -#4888 = EDGE_LOOP('',(#4889,#4890,#4891,#4892)); -#4889 = ORIENTED_EDGE('',*,*,#4776,.F.); -#4890 = ORIENTED_EDGE('',*,*,#4846,.T.); -#4891 = ORIENTED_EDGE('',*,*,#4816,.T.); -#4892 = ORIENTED_EDGE('',*,*,#4868,.F.); -#4893 = PLANE('',#4894); -#4894 = AXIS2_PLACEMENT_3D('',#4895,#4896,#4897); -#4895 = CARTESIAN_POINT('',(36.388888888889,-20.1,8.)); -#4896 = DIRECTION('',(0.,0.,1.)); -#4897 = DIRECTION('',(1.,0.,-0.)); -#4898 = ADVANCED_FACE('',(#4899),#4905,.T.); -#4899 = FACE_BOUND('',#4900,.T.); -#4900 = EDGE_LOOP('',(#4901,#4902,#4903,#4904)); -#4901 = ORIENTED_EDGE('',*,*,#4792,.F.); -#4902 = ORIENTED_EDGE('',*,*,#4853,.T.); -#4903 = ORIENTED_EDGE('',*,*,#4832,.T.); -#4904 = ORIENTED_EDGE('',*,*,#4875,.F.); -#4905 = PLANE('',#4906); -#4906 = AXIS2_PLACEMENT_3D('',#4907,#4908,#4909); -#4907 = CARTESIAN_POINT('',(36.388888888889,-20.1,40.)); -#4908 = DIRECTION('',(0.,0.,1.)); -#4909 = DIRECTION('',(1.,0.,-0.)); -#4910 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4914)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#4911,#4912,#4913)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#4911 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#4912 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#4913 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#4914 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#4911, - 'distance_accuracy_value','confusion accuracy'); -#4915 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#4916,#4918); -#4916 = ( REPRESENTATION_RELATIONSHIP('','',#4759,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#4917) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#4917 = ITEM_DEFINED_TRANSFORMATION('','',#11,#123); -#4918 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #4919); -#4919 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('28','WireDuct_LeftCombSlot_13', - '',#5,#4754,$); -#4920 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4756)); -#4921 = SHAPE_DEFINITION_REPRESENTATION(#4922,#4928); -#4922 = PRODUCT_DEFINITION_SHAPE('','',#4923); -#4923 = PRODUCT_DEFINITION('design','',#4924,#4927); -#4924 = PRODUCT_DEFINITION_FORMATION('','',#4925); -#4925 = PRODUCT('WireDuct_RightCombSlot_13','WireDuct_RightCombSlot_13', - '',(#4926)); -#4926 = PRODUCT_CONTEXT('',#2,'mechanical'); -#4927 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#4928 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#4929),#5079); -#4929 = MANIFOLD_SOLID_BREP('',#4930); -#4930 = CLOSED_SHELL('',(#4931,#4971,#5011,#5033,#5055,#5067)); -#4931 = ADVANCED_FACE('',(#4932),#4966,.F.); -#4932 = FACE_BOUND('',#4933,.F.); -#4933 = EDGE_LOOP('',(#4934,#4944,#4952,#4960)); -#4934 = ORIENTED_EDGE('',*,*,#4935,.F.); -#4935 = EDGE_CURVE('',#4936,#4938,#4940,.T.); -#4936 = VERTEX_POINT('',#4937); -#4937 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); +#4879 = DIRECTION('',(-0.,0.,-1.)); +#4880 = PLANE('',#4881); +#4881 = AXIS2_PLACEMENT_3D('',#4882,#4883,#4884); +#4882 = CARTESIAN_POINT('',(-47.5,17.8,8.)); +#4883 = DIRECTION('',(1.,0.,-0.)); +#4884 = DIRECTION('',(0.,0.,1.)); +#4885 = ADVANCED_FACE('',(#4886),#4904,.T.); +#4886 = FACE_BOUND('',#4887,.T.); +#4887 = EDGE_LOOP('',(#4888,#4896,#4897,#4898)); +#4888 = ORIENTED_EDGE('',*,*,#4889,.F.); +#4889 = EDGE_CURVE('',#2976,#4890,#4892,.T.); +#4890 = VERTEX_POINT('',#4891); +#4891 = CARTESIAN_POINT('',(-52.5,20.,8.)); +#4892 = LINE('',#4893,#4894); +#4893 = CARTESIAN_POINT('',(-52.5,17.8,8.)); +#4894 = VECTOR('',#4895,1.); +#4895 = DIRECTION('',(-0.,1.,0.)); +#4896 = ORIENTED_EDGE('',*,*,#2983,.T.); +#4897 = ORIENTED_EDGE('',*,*,#4867,.T.); +#4898 = ORIENTED_EDGE('',*,*,#4899,.F.); +#4899 = EDGE_CURVE('',#4890,#4868,#4900,.T.); +#4900 = LINE('',#4901,#4902); +#4901 = CARTESIAN_POINT('',(-76.25,20.,8.)); +#4902 = VECTOR('',#4903,1.); +#4903 = DIRECTION('',(1.,0.,-0.)); +#4904 = PLANE('',#4905); +#4905 = AXIS2_PLACEMENT_3D('',#4906,#4907,#4908); +#4906 = CARTESIAN_POINT('',(-52.5,17.8,8.)); +#4907 = DIRECTION('',(0.,0.,1.)); +#4908 = DIRECTION('',(1.,0.,-0.)); +#4909 = ADVANCED_FACE('',(#4910),#4928,.T.); +#4910 = FACE_BOUND('',#4911,.T.); +#4911 = EDGE_LOOP('',(#4912,#4920,#4921,#4922)); +#4912 = ORIENTED_EDGE('',*,*,#4913,.F.); +#4913 = EDGE_CURVE('',#2968,#4914,#4916,.T.); +#4914 = VERTEX_POINT('',#4915); +#4915 = CARTESIAN_POINT('',(-52.5,20.,40.)); +#4916 = LINE('',#4917,#4918); +#4917 = CARTESIAN_POINT('',(-52.5,17.8,40.)); +#4918 = VECTOR('',#4919,1.); +#4919 = DIRECTION('',(-0.,1.,0.)); +#4920 = ORIENTED_EDGE('',*,*,#2975,.T.); +#4921 = ORIENTED_EDGE('',*,*,#4889,.T.); +#4922 = ORIENTED_EDGE('',*,*,#4923,.F.); +#4923 = EDGE_CURVE('',#4914,#4890,#4924,.T.); +#4924 = LINE('',#4925,#4926); +#4925 = CARTESIAN_POINT('',(-52.5,20.,4.)); +#4926 = VECTOR('',#4927,1.); +#4927 = DIRECTION('',(-0.,0.,-1.)); +#4928 = PLANE('',#4929); +#4929 = AXIS2_PLACEMENT_3D('',#4930,#4931,#4932); +#4930 = CARTESIAN_POINT('',(-52.5,17.8,8.)); +#4931 = DIRECTION('',(1.,0.,-0.)); +#4932 = DIRECTION('',(0.,0.,1.)); +#4933 = ADVANCED_FACE('',(#4934),#4952,.T.); +#4934 = FACE_BOUND('',#4935,.T.); +#4935 = EDGE_LOOP('',(#4936,#4944,#4950,#4951)); +#4936 = ORIENTED_EDGE('',*,*,#4937,.F.); +#4937 = EDGE_CURVE('',#4938,#4914,#4940,.T.); #4938 = VERTEX_POINT('',#4939); -#4939 = CARTESIAN_POINT('',(36.388888888889,17.9,40.)); +#4939 = CARTESIAN_POINT('',(-58.61111111111,20.,40.)); #4940 = LINE('',#4941,#4942); -#4941 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); +#4941 = CARTESIAN_POINT('',(-100.,20.,40.)); #4942 = VECTOR('',#4943,1.); -#4943 = DIRECTION('',(0.,0.,1.)); -#4944 = ORIENTED_EDGE('',*,*,#4945,.T.); -#4945 = EDGE_CURVE('',#4936,#4946,#4948,.T.); -#4946 = VERTEX_POINT('',#4947); -#4947 = CARTESIAN_POINT('',(36.388888888889,20.1,8.)); -#4948 = LINE('',#4949,#4950); -#4949 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); -#4950 = VECTOR('',#4951,1.); -#4951 = DIRECTION('',(-0.,1.,0.)); -#4952 = ORIENTED_EDGE('',*,*,#4953,.T.); -#4953 = EDGE_CURVE('',#4946,#4954,#4956,.T.); -#4954 = VERTEX_POINT('',#4955); -#4955 = CARTESIAN_POINT('',(36.388888888889,20.1,40.)); -#4956 = LINE('',#4957,#4958); -#4957 = CARTESIAN_POINT('',(36.388888888889,20.1,8.)); -#4958 = VECTOR('',#4959,1.); -#4959 = DIRECTION('',(0.,0.,1.)); -#4960 = ORIENTED_EDGE('',*,*,#4961,.F.); -#4961 = EDGE_CURVE('',#4938,#4954,#4962,.T.); -#4962 = LINE('',#4963,#4964); -#4963 = CARTESIAN_POINT('',(36.388888888889,17.9,40.)); -#4964 = VECTOR('',#4965,1.); -#4965 = DIRECTION('',(-0.,1.,0.)); -#4966 = PLANE('',#4967); -#4967 = AXIS2_PLACEMENT_3D('',#4968,#4969,#4970); -#4968 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); -#4969 = DIRECTION('',(1.,0.,-0.)); -#4970 = DIRECTION('',(0.,0.,1.)); -#4971 = ADVANCED_FACE('',(#4972),#5006,.T.); -#4972 = FACE_BOUND('',#4973,.T.); -#4973 = EDGE_LOOP('',(#4974,#4984,#4992,#5000)); -#4974 = ORIENTED_EDGE('',*,*,#4975,.F.); -#4975 = EDGE_CURVE('',#4976,#4978,#4980,.T.); -#4976 = VERTEX_POINT('',#4977); -#4977 = CARTESIAN_POINT('',(41.388888888889,17.9,8.)); -#4978 = VERTEX_POINT('',#4979); -#4979 = CARTESIAN_POINT('',(41.388888888889,17.9,40.)); -#4980 = LINE('',#4981,#4982); -#4981 = CARTESIAN_POINT('',(41.388888888889,17.9,8.)); -#4982 = VECTOR('',#4983,1.); -#4983 = DIRECTION('',(0.,0.,1.)); -#4984 = ORIENTED_EDGE('',*,*,#4985,.T.); -#4985 = EDGE_CURVE('',#4976,#4986,#4988,.T.); +#4943 = DIRECTION('',(1.,0.,-0.)); +#4944 = ORIENTED_EDGE('',*,*,#4945,.F.); +#4945 = EDGE_CURVE('',#2960,#4938,#4946,.T.); +#4946 = LINE('',#4947,#4948); +#4947 = CARTESIAN_POINT('',(-58.61111111111,17.8,40.)); +#4948 = VECTOR('',#4949,1.); +#4949 = DIRECTION('',(-0.,1.,0.)); +#4950 = ORIENTED_EDGE('',*,*,#2967,.T.); +#4951 = ORIENTED_EDGE('',*,*,#4913,.T.); +#4952 = PLANE('',#4953); +#4953 = AXIS2_PLACEMENT_3D('',#4954,#4955,#4956); +#4954 = CARTESIAN_POINT('',(-100.,18.,40.)); +#4955 = DIRECTION('',(0.,0.,1.)); +#4956 = DIRECTION('',(1.,0.,-0.)); +#4957 = ADVANCED_FACE('',(#4958),#4976,.F.); +#4958 = FACE_BOUND('',#4959,.F.); +#4959 = EDGE_LOOP('',(#4960,#4961,#4962,#4970)); +#4960 = ORIENTED_EDGE('',*,*,#4945,.F.); +#4961 = ORIENTED_EDGE('',*,*,#2959,.T.); +#4962 = ORIENTED_EDGE('',*,*,#4963,.T.); +#4963 = EDGE_CURVE('',#2952,#4964,#4966,.T.); +#4964 = VERTEX_POINT('',#4965); +#4965 = CARTESIAN_POINT('',(-58.61111111111,20.,8.)); +#4966 = LINE('',#4967,#4968); +#4967 = CARTESIAN_POINT('',(-58.61111111111,17.8,8.)); +#4968 = VECTOR('',#4969,1.); +#4969 = DIRECTION('',(-0.,1.,0.)); +#4970 = ORIENTED_EDGE('',*,*,#4971,.F.); +#4971 = EDGE_CURVE('',#4938,#4964,#4972,.T.); +#4972 = LINE('',#4973,#4974); +#4973 = CARTESIAN_POINT('',(-58.61111111111,20.,4.)); +#4974 = VECTOR('',#4975,1.); +#4975 = DIRECTION('',(-0.,0.,-1.)); +#4976 = PLANE('',#4977); +#4977 = AXIS2_PLACEMENT_3D('',#4978,#4979,#4980); +#4978 = CARTESIAN_POINT('',(-58.61111111111,17.8,8.)); +#4979 = DIRECTION('',(1.,0.,-0.)); +#4980 = DIRECTION('',(0.,0.,1.)); +#4981 = ADVANCED_FACE('',(#4982),#5000,.T.); +#4982 = FACE_BOUND('',#4983,.T.); +#4983 = EDGE_LOOP('',(#4984,#4992,#4993,#4994)); +#4984 = ORIENTED_EDGE('',*,*,#4985,.F.); +#4985 = EDGE_CURVE('',#2944,#4986,#4988,.T.); #4986 = VERTEX_POINT('',#4987); -#4987 = CARTESIAN_POINT('',(41.388888888889,20.1,8.)); +#4987 = CARTESIAN_POINT('',(-63.61111111111,20.,8.)); #4988 = LINE('',#4989,#4990); -#4989 = CARTESIAN_POINT('',(41.388888888889,17.9,8.)); +#4989 = CARTESIAN_POINT('',(-63.61111111111,17.8,8.)); #4990 = VECTOR('',#4991,1.); #4991 = DIRECTION('',(-0.,1.,0.)); -#4992 = ORIENTED_EDGE('',*,*,#4993,.T.); -#4993 = EDGE_CURVE('',#4986,#4994,#4996,.T.); -#4994 = VERTEX_POINT('',#4995); -#4995 = CARTESIAN_POINT('',(41.388888888889,20.1,40.)); +#4992 = ORIENTED_EDGE('',*,*,#2951,.T.); +#4993 = ORIENTED_EDGE('',*,*,#4963,.T.); +#4994 = ORIENTED_EDGE('',*,*,#4995,.F.); +#4995 = EDGE_CURVE('',#4986,#4964,#4996,.T.); #4996 = LINE('',#4997,#4998); -#4997 = CARTESIAN_POINT('',(41.388888888889,20.1,8.)); +#4997 = CARTESIAN_POINT('',(-81.80555555555,20.,8.)); #4998 = VECTOR('',#4999,1.); -#4999 = DIRECTION('',(0.,0.,1.)); -#5000 = ORIENTED_EDGE('',*,*,#5001,.F.); -#5001 = EDGE_CURVE('',#4978,#4994,#5002,.T.); -#5002 = LINE('',#5003,#5004); -#5003 = CARTESIAN_POINT('',(41.388888888889,17.9,40.)); -#5004 = VECTOR('',#5005,1.); -#5005 = DIRECTION('',(-0.,1.,0.)); -#5006 = PLANE('',#5007); -#5007 = AXIS2_PLACEMENT_3D('',#5008,#5009,#5010); -#5008 = CARTESIAN_POINT('',(41.388888888889,17.9,8.)); -#5009 = DIRECTION('',(1.,0.,-0.)); -#5010 = DIRECTION('',(0.,0.,1.)); -#5011 = ADVANCED_FACE('',(#5012),#5028,.F.); -#5012 = FACE_BOUND('',#5013,.F.); -#5013 = EDGE_LOOP('',(#5014,#5020,#5021,#5027)); -#5014 = ORIENTED_EDGE('',*,*,#5015,.F.); -#5015 = EDGE_CURVE('',#4936,#4976,#5016,.T.); -#5016 = LINE('',#5017,#5018); -#5017 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); -#5018 = VECTOR('',#5019,1.); -#5019 = DIRECTION('',(1.,0.,-0.)); -#5020 = ORIENTED_EDGE('',*,*,#4935,.T.); -#5021 = ORIENTED_EDGE('',*,*,#5022,.T.); -#5022 = EDGE_CURVE('',#4938,#4978,#5023,.T.); -#5023 = LINE('',#5024,#5025); -#5024 = CARTESIAN_POINT('',(36.388888888889,17.9,40.)); -#5025 = VECTOR('',#5026,1.); -#5026 = DIRECTION('',(1.,0.,-0.)); -#5027 = ORIENTED_EDGE('',*,*,#4975,.F.); -#5028 = PLANE('',#5029); -#5029 = AXIS2_PLACEMENT_3D('',#5030,#5031,#5032); -#5030 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); -#5031 = DIRECTION('',(-0.,1.,0.)); -#5032 = DIRECTION('',(0.,0.,1.)); -#5033 = ADVANCED_FACE('',(#5034),#5050,.T.); -#5034 = FACE_BOUND('',#5035,.T.); -#5035 = EDGE_LOOP('',(#5036,#5042,#5043,#5049)); -#5036 = ORIENTED_EDGE('',*,*,#5037,.F.); -#5037 = EDGE_CURVE('',#4946,#4986,#5038,.T.); -#5038 = LINE('',#5039,#5040); -#5039 = CARTESIAN_POINT('',(36.388888888889,20.1,8.)); -#5040 = VECTOR('',#5041,1.); -#5041 = DIRECTION('',(1.,0.,-0.)); -#5042 = ORIENTED_EDGE('',*,*,#4953,.T.); -#5043 = ORIENTED_EDGE('',*,*,#5044,.T.); -#5044 = EDGE_CURVE('',#4954,#4994,#5045,.T.); -#5045 = LINE('',#5046,#5047); -#5046 = CARTESIAN_POINT('',(36.388888888889,20.1,40.)); -#5047 = VECTOR('',#5048,1.); -#5048 = DIRECTION('',(1.,0.,-0.)); -#5049 = ORIENTED_EDGE('',*,*,#4993,.F.); -#5050 = PLANE('',#5051); -#5051 = AXIS2_PLACEMENT_3D('',#5052,#5053,#5054); -#5052 = CARTESIAN_POINT('',(36.388888888889,20.1,8.)); -#5053 = DIRECTION('',(-0.,1.,0.)); -#5054 = DIRECTION('',(0.,0.,1.)); -#5055 = ADVANCED_FACE('',(#5056),#5062,.F.); -#5056 = FACE_BOUND('',#5057,.F.); -#5057 = EDGE_LOOP('',(#5058,#5059,#5060,#5061)); -#5058 = ORIENTED_EDGE('',*,*,#4945,.F.); -#5059 = ORIENTED_EDGE('',*,*,#5015,.T.); -#5060 = ORIENTED_EDGE('',*,*,#4985,.T.); -#5061 = ORIENTED_EDGE('',*,*,#5037,.F.); -#5062 = PLANE('',#5063); -#5063 = AXIS2_PLACEMENT_3D('',#5064,#5065,#5066); -#5064 = CARTESIAN_POINT('',(36.388888888889,17.9,8.)); -#5065 = DIRECTION('',(0.,0.,1.)); -#5066 = DIRECTION('',(1.,0.,-0.)); -#5067 = ADVANCED_FACE('',(#5068),#5074,.T.); -#5068 = FACE_BOUND('',#5069,.T.); -#5069 = EDGE_LOOP('',(#5070,#5071,#5072,#5073)); -#5070 = ORIENTED_EDGE('',*,*,#4961,.F.); -#5071 = ORIENTED_EDGE('',*,*,#5022,.T.); -#5072 = ORIENTED_EDGE('',*,*,#5001,.T.); -#5073 = ORIENTED_EDGE('',*,*,#5044,.F.); -#5074 = PLANE('',#5075); -#5075 = AXIS2_PLACEMENT_3D('',#5076,#5077,#5078); -#5076 = CARTESIAN_POINT('',(36.388888888889,17.9,40.)); -#5077 = DIRECTION('',(0.,0.,1.)); -#5078 = DIRECTION('',(1.,0.,-0.)); -#5079 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5083)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#5080,#5081,#5082)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#5080 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#5081 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#5082 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#5083 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5080, - 'distance_accuracy_value','confusion accuracy'); -#5084 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5085,#5087); -#5085 = ( REPRESENTATION_RELATIONSHIP('','',#4928,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5086) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#5086 = ITEM_DEFINED_TRANSFORMATION('','',#11,#127); -#5087 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #5088); -#5088 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('29','WireDuct_RightCombSlot_13', - '',#5,#4923,$); -#5089 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#4925)); -#5090 = SHAPE_DEFINITION_REPRESENTATION(#5091,#5097); -#5091 = PRODUCT_DEFINITION_SHAPE('','',#5092); -#5092 = PRODUCT_DEFINITION('design','',#5093,#5096); -#5093 = PRODUCT_DEFINITION_FORMATION('','',#5094); -#5094 = PRODUCT('WireDuct_LeftCombSlot_14','WireDuct_LeftCombSlot_14','' - ,(#5095)); -#5095 = PRODUCT_CONTEXT('',#2,'mechanical'); -#5096 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#5097 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5098),#5248); -#5098 = MANIFOLD_SOLID_BREP('',#5099); -#5099 = CLOSED_SHELL('',(#5100,#5140,#5180,#5202,#5224,#5236)); -#5100 = ADVANCED_FACE('',(#5101),#5135,.F.); -#5101 = FACE_BOUND('',#5102,.F.); -#5102 = EDGE_LOOP('',(#5103,#5113,#5121,#5129)); -#5103 = ORIENTED_EDGE('',*,*,#5104,.F.); -#5104 = EDGE_CURVE('',#5105,#5107,#5109,.T.); -#5105 = VERTEX_POINT('',#5106); -#5106 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5107 = VERTEX_POINT('',#5108); -#5108 = CARTESIAN_POINT('',(47.5,-20.1,40.)); -#5109 = LINE('',#5110,#5111); -#5110 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5111 = VECTOR('',#5112,1.); -#5112 = DIRECTION('',(0.,0.,1.)); -#5113 = ORIENTED_EDGE('',*,*,#5114,.T.); -#5114 = EDGE_CURVE('',#5105,#5115,#5117,.T.); -#5115 = VERTEX_POINT('',#5116); -#5116 = CARTESIAN_POINT('',(47.5,-17.9,8.)); -#5117 = LINE('',#5118,#5119); -#5118 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5119 = VECTOR('',#5120,1.); -#5120 = DIRECTION('',(-0.,1.,0.)); -#5121 = ORIENTED_EDGE('',*,*,#5122,.T.); -#5122 = EDGE_CURVE('',#5115,#5123,#5125,.T.); -#5123 = VERTEX_POINT('',#5124); -#5124 = CARTESIAN_POINT('',(47.5,-17.9,40.)); -#5125 = LINE('',#5126,#5127); -#5126 = CARTESIAN_POINT('',(47.5,-17.9,8.)); -#5127 = VECTOR('',#5128,1.); -#5128 = DIRECTION('',(0.,0.,1.)); -#5129 = ORIENTED_EDGE('',*,*,#5130,.F.); -#5130 = EDGE_CURVE('',#5107,#5123,#5131,.T.); -#5131 = LINE('',#5132,#5133); -#5132 = CARTESIAN_POINT('',(47.5,-20.1,40.)); -#5133 = VECTOR('',#5134,1.); -#5134 = DIRECTION('',(-0.,1.,0.)); -#5135 = PLANE('',#5136); -#5136 = AXIS2_PLACEMENT_3D('',#5137,#5138,#5139); -#5137 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5138 = DIRECTION('',(1.,0.,-0.)); -#5139 = DIRECTION('',(0.,0.,1.)); -#5140 = ADVANCED_FACE('',(#5141),#5175,.T.); -#5141 = FACE_BOUND('',#5142,.T.); -#5142 = EDGE_LOOP('',(#5143,#5153,#5161,#5169)); -#5143 = ORIENTED_EDGE('',*,*,#5144,.F.); -#5144 = EDGE_CURVE('',#5145,#5147,#5149,.T.); -#5145 = VERTEX_POINT('',#5146); -#5146 = CARTESIAN_POINT('',(52.5,-20.1,8.)); -#5147 = VERTEX_POINT('',#5148); -#5148 = CARTESIAN_POINT('',(52.5,-20.1,40.)); -#5149 = LINE('',#5150,#5151); -#5150 = CARTESIAN_POINT('',(52.5,-20.1,8.)); -#5151 = VECTOR('',#5152,1.); -#5152 = DIRECTION('',(0.,0.,1.)); -#5153 = ORIENTED_EDGE('',*,*,#5154,.T.); -#5154 = EDGE_CURVE('',#5145,#5155,#5157,.T.); -#5155 = VERTEX_POINT('',#5156); -#5156 = CARTESIAN_POINT('',(52.5,-17.9,8.)); -#5157 = LINE('',#5158,#5159); -#5158 = CARTESIAN_POINT('',(52.5,-20.1,8.)); -#5159 = VECTOR('',#5160,1.); -#5160 = DIRECTION('',(-0.,1.,0.)); -#5161 = ORIENTED_EDGE('',*,*,#5162,.T.); -#5162 = EDGE_CURVE('',#5155,#5163,#5165,.T.); -#5163 = VERTEX_POINT('',#5164); -#5164 = CARTESIAN_POINT('',(52.5,-17.9,40.)); -#5165 = LINE('',#5166,#5167); -#5166 = CARTESIAN_POINT('',(52.5,-17.9,8.)); -#5167 = VECTOR('',#5168,1.); -#5168 = DIRECTION('',(0.,0.,1.)); -#5169 = ORIENTED_EDGE('',*,*,#5170,.F.); -#5170 = EDGE_CURVE('',#5147,#5163,#5171,.T.); -#5171 = LINE('',#5172,#5173); -#5172 = CARTESIAN_POINT('',(52.5,-20.1,40.)); -#5173 = VECTOR('',#5174,1.); -#5174 = DIRECTION('',(-0.,1.,0.)); -#5175 = PLANE('',#5176); -#5176 = AXIS2_PLACEMENT_3D('',#5177,#5178,#5179); -#5177 = CARTESIAN_POINT('',(52.5,-20.1,8.)); -#5178 = DIRECTION('',(1.,0.,-0.)); -#5179 = DIRECTION('',(0.,0.,1.)); -#5180 = ADVANCED_FACE('',(#5181),#5197,.F.); -#5181 = FACE_BOUND('',#5182,.F.); -#5182 = EDGE_LOOP('',(#5183,#5189,#5190,#5196)); -#5183 = ORIENTED_EDGE('',*,*,#5184,.F.); -#5184 = EDGE_CURVE('',#5105,#5145,#5185,.T.); -#5185 = LINE('',#5186,#5187); -#5186 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5187 = VECTOR('',#5188,1.); -#5188 = DIRECTION('',(1.,0.,-0.)); -#5189 = ORIENTED_EDGE('',*,*,#5104,.T.); -#5190 = ORIENTED_EDGE('',*,*,#5191,.T.); -#5191 = EDGE_CURVE('',#5107,#5147,#5192,.T.); -#5192 = LINE('',#5193,#5194); -#5193 = CARTESIAN_POINT('',(47.5,-20.1,40.)); -#5194 = VECTOR('',#5195,1.); -#5195 = DIRECTION('',(1.,0.,-0.)); -#5196 = ORIENTED_EDGE('',*,*,#5144,.F.); -#5197 = PLANE('',#5198); -#5198 = AXIS2_PLACEMENT_3D('',#5199,#5200,#5201); -#5199 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5200 = DIRECTION('',(-0.,1.,0.)); -#5201 = DIRECTION('',(0.,0.,1.)); -#5202 = ADVANCED_FACE('',(#5203),#5219,.T.); -#5203 = FACE_BOUND('',#5204,.T.); -#5204 = EDGE_LOOP('',(#5205,#5211,#5212,#5218)); -#5205 = ORIENTED_EDGE('',*,*,#5206,.F.); -#5206 = EDGE_CURVE('',#5115,#5155,#5207,.T.); -#5207 = LINE('',#5208,#5209); -#5208 = CARTESIAN_POINT('',(47.5,-17.9,8.)); -#5209 = VECTOR('',#5210,1.); -#5210 = DIRECTION('',(1.,0.,-0.)); -#5211 = ORIENTED_EDGE('',*,*,#5122,.T.); -#5212 = ORIENTED_EDGE('',*,*,#5213,.T.); -#5213 = EDGE_CURVE('',#5123,#5163,#5214,.T.); -#5214 = LINE('',#5215,#5216); -#5215 = CARTESIAN_POINT('',(47.5,-17.9,40.)); -#5216 = VECTOR('',#5217,1.); -#5217 = DIRECTION('',(1.,0.,-0.)); -#5218 = ORIENTED_EDGE('',*,*,#5162,.F.); -#5219 = PLANE('',#5220); -#5220 = AXIS2_PLACEMENT_3D('',#5221,#5222,#5223); -#5221 = CARTESIAN_POINT('',(47.5,-17.9,8.)); -#5222 = DIRECTION('',(-0.,1.,0.)); -#5223 = DIRECTION('',(0.,0.,1.)); -#5224 = ADVANCED_FACE('',(#5225),#5231,.F.); -#5225 = FACE_BOUND('',#5226,.F.); -#5226 = EDGE_LOOP('',(#5227,#5228,#5229,#5230)); -#5227 = ORIENTED_EDGE('',*,*,#5114,.F.); -#5228 = ORIENTED_EDGE('',*,*,#5184,.T.); -#5229 = ORIENTED_EDGE('',*,*,#5154,.T.); -#5230 = ORIENTED_EDGE('',*,*,#5206,.F.); -#5231 = PLANE('',#5232); -#5232 = AXIS2_PLACEMENT_3D('',#5233,#5234,#5235); -#5233 = CARTESIAN_POINT('',(47.5,-20.1,8.)); -#5234 = DIRECTION('',(0.,0.,1.)); -#5235 = DIRECTION('',(1.,0.,-0.)); -#5236 = ADVANCED_FACE('',(#5237),#5243,.T.); -#5237 = FACE_BOUND('',#5238,.T.); -#5238 = EDGE_LOOP('',(#5239,#5240,#5241,#5242)); -#5239 = ORIENTED_EDGE('',*,*,#5130,.F.); -#5240 = ORIENTED_EDGE('',*,*,#5191,.T.); -#5241 = ORIENTED_EDGE('',*,*,#5170,.T.); -#5242 = ORIENTED_EDGE('',*,*,#5213,.F.); -#5243 = PLANE('',#5244); -#5244 = AXIS2_PLACEMENT_3D('',#5245,#5246,#5247); -#5245 = CARTESIAN_POINT('',(47.5,-20.1,40.)); -#5246 = DIRECTION('',(0.,0.,1.)); -#5247 = DIRECTION('',(1.,0.,-0.)); -#5248 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5252)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#5249,#5250,#5251)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#5249 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#5250 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#5251 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#5252 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5249, - 'distance_accuracy_value','confusion accuracy'); -#5253 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5254,#5256); -#5254 = ( REPRESENTATION_RELATIONSHIP('','',#5097,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5255) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#5255 = ITEM_DEFINED_TRANSFORMATION('','',#11,#131); -#5256 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #5257); -#5257 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('30','WireDuct_LeftCombSlot_14', - '',#5,#5092,$); -#5258 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5094)); -#5259 = SHAPE_DEFINITION_REPRESENTATION(#5260,#5266); -#5260 = PRODUCT_DEFINITION_SHAPE('','',#5261); -#5261 = PRODUCT_DEFINITION('design','',#5262,#5265); -#5262 = PRODUCT_DEFINITION_FORMATION('','',#5263); -#5263 = PRODUCT('WireDuct_RightCombSlot_14','WireDuct_RightCombSlot_14', - '',(#5264)); -#5264 = PRODUCT_CONTEXT('',#2,'mechanical'); -#5265 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#5266 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5267),#5417); -#5267 = MANIFOLD_SOLID_BREP('',#5268); -#5268 = CLOSED_SHELL('',(#5269,#5309,#5349,#5371,#5393,#5405)); -#5269 = ADVANCED_FACE('',(#5270),#5304,.F.); -#5270 = FACE_BOUND('',#5271,.F.); -#5271 = EDGE_LOOP('',(#5272,#5282,#5290,#5298)); +#4999 = DIRECTION('',(1.,0.,-0.)); +#5000 = PLANE('',#5001); +#5001 = AXIS2_PLACEMENT_3D('',#5002,#5003,#5004); +#5002 = CARTESIAN_POINT('',(-63.61111111111,17.8,8.)); +#5003 = DIRECTION('',(0.,0.,1.)); +#5004 = DIRECTION('',(1.,0.,-0.)); +#5005 = ADVANCED_FACE('',(#5006),#5024,.T.); +#5006 = FACE_BOUND('',#5007,.T.); +#5007 = EDGE_LOOP('',(#5008,#5016,#5017,#5018)); +#5008 = ORIENTED_EDGE('',*,*,#5009,.F.); +#5009 = EDGE_CURVE('',#2936,#5010,#5012,.T.); +#5010 = VERTEX_POINT('',#5011); +#5011 = CARTESIAN_POINT('',(-63.61111111111,20.,40.)); +#5012 = LINE('',#5013,#5014); +#5013 = CARTESIAN_POINT('',(-63.61111111111,17.8,40.)); +#5014 = VECTOR('',#5015,1.); +#5015 = DIRECTION('',(-0.,1.,0.)); +#5016 = ORIENTED_EDGE('',*,*,#2943,.T.); +#5017 = ORIENTED_EDGE('',*,*,#4985,.T.); +#5018 = ORIENTED_EDGE('',*,*,#5019,.F.); +#5019 = EDGE_CURVE('',#5010,#4986,#5020,.T.); +#5020 = LINE('',#5021,#5022); +#5021 = CARTESIAN_POINT('',(-63.61111111111,20.,4.)); +#5022 = VECTOR('',#5023,1.); +#5023 = DIRECTION('',(-0.,0.,-1.)); +#5024 = PLANE('',#5025); +#5025 = AXIS2_PLACEMENT_3D('',#5026,#5027,#5028); +#5026 = CARTESIAN_POINT('',(-63.61111111111,17.8,8.)); +#5027 = DIRECTION('',(1.,0.,-0.)); +#5028 = DIRECTION('',(0.,0.,1.)); +#5029 = ADVANCED_FACE('',(#5030),#5048,.T.); +#5030 = FACE_BOUND('',#5031,.T.); +#5031 = EDGE_LOOP('',(#5032,#5040,#5046,#5047)); +#5032 = ORIENTED_EDGE('',*,*,#5033,.F.); +#5033 = EDGE_CURVE('',#5034,#5010,#5036,.T.); +#5034 = VERTEX_POINT('',#5035); +#5035 = CARTESIAN_POINT('',(-69.72222222222,20.,40.)); +#5036 = LINE('',#5037,#5038); +#5037 = CARTESIAN_POINT('',(-100.,20.,40.)); +#5038 = VECTOR('',#5039,1.); +#5039 = DIRECTION('',(1.,0.,-0.)); +#5040 = ORIENTED_EDGE('',*,*,#5041,.F.); +#5041 = EDGE_CURVE('',#2928,#5034,#5042,.T.); +#5042 = LINE('',#5043,#5044); +#5043 = CARTESIAN_POINT('',(-69.72222222222,17.8,40.)); +#5044 = VECTOR('',#5045,1.); +#5045 = DIRECTION('',(-0.,1.,0.)); +#5046 = ORIENTED_EDGE('',*,*,#2935,.T.); +#5047 = ORIENTED_EDGE('',*,*,#5009,.T.); +#5048 = PLANE('',#5049); +#5049 = AXIS2_PLACEMENT_3D('',#5050,#5051,#5052); +#5050 = CARTESIAN_POINT('',(-100.,18.,40.)); +#5051 = DIRECTION('',(0.,0.,1.)); +#5052 = DIRECTION('',(1.,0.,-0.)); +#5053 = ADVANCED_FACE('',(#5054),#5072,.F.); +#5054 = FACE_BOUND('',#5055,.F.); +#5055 = EDGE_LOOP('',(#5056,#5057,#5058,#5066)); +#5056 = ORIENTED_EDGE('',*,*,#5041,.F.); +#5057 = ORIENTED_EDGE('',*,*,#2927,.T.); +#5058 = ORIENTED_EDGE('',*,*,#5059,.T.); +#5059 = EDGE_CURVE('',#2920,#5060,#5062,.T.); +#5060 = VERTEX_POINT('',#5061); +#5061 = CARTESIAN_POINT('',(-69.72222222222,20.,8.)); +#5062 = LINE('',#5063,#5064); +#5063 = CARTESIAN_POINT('',(-69.72222222222,17.8,8.)); +#5064 = VECTOR('',#5065,1.); +#5065 = DIRECTION('',(-0.,1.,0.)); +#5066 = ORIENTED_EDGE('',*,*,#5067,.F.); +#5067 = EDGE_CURVE('',#5034,#5060,#5068,.T.); +#5068 = LINE('',#5069,#5070); +#5069 = CARTESIAN_POINT('',(-69.72222222222,20.,4.)); +#5070 = VECTOR('',#5071,1.); +#5071 = DIRECTION('',(-0.,0.,-1.)); +#5072 = PLANE('',#5073); +#5073 = AXIS2_PLACEMENT_3D('',#5074,#5075,#5076); +#5074 = CARTESIAN_POINT('',(-69.72222222222,17.8,8.)); +#5075 = DIRECTION('',(1.,0.,-0.)); +#5076 = DIRECTION('',(0.,0.,1.)); +#5077 = ADVANCED_FACE('',(#5078),#5096,.T.); +#5078 = FACE_BOUND('',#5079,.T.); +#5079 = EDGE_LOOP('',(#5080,#5088,#5089,#5090)); +#5080 = ORIENTED_EDGE('',*,*,#5081,.F.); +#5081 = EDGE_CURVE('',#2912,#5082,#5084,.T.); +#5082 = VERTEX_POINT('',#5083); +#5083 = CARTESIAN_POINT('',(-74.72222222222,20.,8.)); +#5084 = LINE('',#5085,#5086); +#5085 = CARTESIAN_POINT('',(-74.72222222222,17.8,8.)); +#5086 = VECTOR('',#5087,1.); +#5087 = DIRECTION('',(-0.,1.,0.)); +#5088 = ORIENTED_EDGE('',*,*,#2919,.T.); +#5089 = ORIENTED_EDGE('',*,*,#5059,.T.); +#5090 = ORIENTED_EDGE('',*,*,#5091,.F.); +#5091 = EDGE_CURVE('',#5082,#5060,#5092,.T.); +#5092 = LINE('',#5093,#5094); +#5093 = CARTESIAN_POINT('',(-87.36111111111,20.,8.)); +#5094 = VECTOR('',#5095,1.); +#5095 = DIRECTION('',(1.,0.,-0.)); +#5096 = PLANE('',#5097); +#5097 = AXIS2_PLACEMENT_3D('',#5098,#5099,#5100); +#5098 = CARTESIAN_POINT('',(-74.72222222222,17.8,8.)); +#5099 = DIRECTION('',(0.,0.,1.)); +#5100 = DIRECTION('',(1.,0.,-0.)); +#5101 = ADVANCED_FACE('',(#5102),#5120,.T.); +#5102 = FACE_BOUND('',#5103,.T.); +#5103 = EDGE_LOOP('',(#5104,#5112,#5113,#5114)); +#5104 = ORIENTED_EDGE('',*,*,#5105,.F.); +#5105 = EDGE_CURVE('',#2904,#5106,#5108,.T.); +#5106 = VERTEX_POINT('',#5107); +#5107 = CARTESIAN_POINT('',(-74.72222222222,20.,40.)); +#5108 = LINE('',#5109,#5110); +#5109 = CARTESIAN_POINT('',(-74.72222222222,17.8,40.)); +#5110 = VECTOR('',#5111,1.); +#5111 = DIRECTION('',(-0.,1.,0.)); +#5112 = ORIENTED_EDGE('',*,*,#2911,.T.); +#5113 = ORIENTED_EDGE('',*,*,#5081,.T.); +#5114 = ORIENTED_EDGE('',*,*,#5115,.F.); +#5115 = EDGE_CURVE('',#5106,#5082,#5116,.T.); +#5116 = LINE('',#5117,#5118); +#5117 = CARTESIAN_POINT('',(-74.72222222222,20.,4.)); +#5118 = VECTOR('',#5119,1.); +#5119 = DIRECTION('',(-0.,0.,-1.)); +#5120 = PLANE('',#5121); +#5121 = AXIS2_PLACEMENT_3D('',#5122,#5123,#5124); +#5122 = CARTESIAN_POINT('',(-74.72222222222,17.8,8.)); +#5123 = DIRECTION('',(1.,0.,-0.)); +#5124 = DIRECTION('',(0.,0.,1.)); +#5125 = ADVANCED_FACE('',(#5126),#5144,.T.); +#5126 = FACE_BOUND('',#5127,.T.); +#5127 = EDGE_LOOP('',(#5128,#5136,#5142,#5143)); +#5128 = ORIENTED_EDGE('',*,*,#5129,.F.); +#5129 = EDGE_CURVE('',#5130,#5106,#5132,.T.); +#5130 = VERTEX_POINT('',#5131); +#5131 = CARTESIAN_POINT('',(-80.83333333333,20.,40.)); +#5132 = LINE('',#5133,#5134); +#5133 = CARTESIAN_POINT('',(-100.,20.,40.)); +#5134 = VECTOR('',#5135,1.); +#5135 = DIRECTION('',(1.,0.,-0.)); +#5136 = ORIENTED_EDGE('',*,*,#5137,.F.); +#5137 = EDGE_CURVE('',#2896,#5130,#5138,.T.); +#5138 = LINE('',#5139,#5140); +#5139 = CARTESIAN_POINT('',(-80.83333333333,17.8,40.)); +#5140 = VECTOR('',#5141,1.); +#5141 = DIRECTION('',(-0.,1.,0.)); +#5142 = ORIENTED_EDGE('',*,*,#2903,.T.); +#5143 = ORIENTED_EDGE('',*,*,#5105,.T.); +#5144 = PLANE('',#5145); +#5145 = AXIS2_PLACEMENT_3D('',#5146,#5147,#5148); +#5146 = CARTESIAN_POINT('',(-100.,18.,40.)); +#5147 = DIRECTION('',(0.,0.,1.)); +#5148 = DIRECTION('',(1.,0.,-0.)); +#5149 = ADVANCED_FACE('',(#5150),#5168,.F.); +#5150 = FACE_BOUND('',#5151,.F.); +#5151 = EDGE_LOOP('',(#5152,#5153,#5154,#5162)); +#5152 = ORIENTED_EDGE('',*,*,#5137,.F.); +#5153 = ORIENTED_EDGE('',*,*,#2895,.T.); +#5154 = ORIENTED_EDGE('',*,*,#5155,.T.); +#5155 = EDGE_CURVE('',#2888,#5156,#5158,.T.); +#5156 = VERTEX_POINT('',#5157); +#5157 = CARTESIAN_POINT('',(-80.83333333333,20.,8.)); +#5158 = LINE('',#5159,#5160); +#5159 = CARTESIAN_POINT('',(-80.83333333333,17.8,8.)); +#5160 = VECTOR('',#5161,1.); +#5161 = DIRECTION('',(-0.,1.,0.)); +#5162 = ORIENTED_EDGE('',*,*,#5163,.F.); +#5163 = EDGE_CURVE('',#5130,#5156,#5164,.T.); +#5164 = LINE('',#5165,#5166); +#5165 = CARTESIAN_POINT('',(-80.83333333333,20.,4.)); +#5166 = VECTOR('',#5167,1.); +#5167 = DIRECTION('',(-0.,0.,-1.)); +#5168 = PLANE('',#5169); +#5169 = AXIS2_PLACEMENT_3D('',#5170,#5171,#5172); +#5170 = CARTESIAN_POINT('',(-80.83333333333,17.8,8.)); +#5171 = DIRECTION('',(1.,0.,-0.)); +#5172 = DIRECTION('',(0.,0.,1.)); +#5173 = ADVANCED_FACE('',(#5174),#5192,.T.); +#5174 = FACE_BOUND('',#5175,.T.); +#5175 = EDGE_LOOP('',(#5176,#5184,#5185,#5186)); +#5176 = ORIENTED_EDGE('',*,*,#5177,.F.); +#5177 = EDGE_CURVE('',#2880,#5178,#5180,.T.); +#5178 = VERTEX_POINT('',#5179); +#5179 = CARTESIAN_POINT('',(-85.83333333333,20.,8.)); +#5180 = LINE('',#5181,#5182); +#5181 = CARTESIAN_POINT('',(-85.83333333333,17.8,8.)); +#5182 = VECTOR('',#5183,1.); +#5183 = DIRECTION('',(-0.,1.,0.)); +#5184 = ORIENTED_EDGE('',*,*,#2887,.T.); +#5185 = ORIENTED_EDGE('',*,*,#5155,.T.); +#5186 = ORIENTED_EDGE('',*,*,#5187,.F.); +#5187 = EDGE_CURVE('',#5178,#5156,#5188,.T.); +#5188 = LINE('',#5189,#5190); +#5189 = CARTESIAN_POINT('',(-92.91666666666,20.,8.)); +#5190 = VECTOR('',#5191,1.); +#5191 = DIRECTION('',(1.,0.,-0.)); +#5192 = PLANE('',#5193); +#5193 = AXIS2_PLACEMENT_3D('',#5194,#5195,#5196); +#5194 = CARTESIAN_POINT('',(-85.83333333333,17.8,8.)); +#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,#5208,#5209,#5210)); +#5200 = ORIENTED_EDGE('',*,*,#5201,.F.); +#5201 = EDGE_CURVE('',#2872,#5202,#5204,.T.); +#5202 = VERTEX_POINT('',#5203); +#5203 = CARTESIAN_POINT('',(-85.83333333333,20.,40.)); +#5204 = LINE('',#5205,#5206); +#5205 = CARTESIAN_POINT('',(-85.83333333333,17.8,40.)); +#5206 = VECTOR('',#5207,1.); +#5207 = DIRECTION('',(-0.,1.,0.)); +#5208 = ORIENTED_EDGE('',*,*,#2879,.T.); +#5209 = ORIENTED_EDGE('',*,*,#5177,.T.); +#5210 = ORIENTED_EDGE('',*,*,#5211,.F.); +#5211 = EDGE_CURVE('',#5202,#5178,#5212,.T.); +#5212 = LINE('',#5213,#5214); +#5213 = CARTESIAN_POINT('',(-85.83333333333,20.,4.)); +#5214 = VECTOR('',#5215,1.); +#5215 = DIRECTION('',(-0.,0.,-1.)); +#5216 = PLANE('',#5217); +#5217 = AXIS2_PLACEMENT_3D('',#5218,#5219,#5220); +#5218 = CARTESIAN_POINT('',(-85.83333333333,17.8,8.)); +#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,#5202,#5228,.T.); +#5226 = VERTEX_POINT('',#5227); +#5227 = CARTESIAN_POINT('',(-91.94444444444,20.,40.)); +#5228 = LINE('',#5229,#5230); +#5229 = CARTESIAN_POINT('',(-100.,20.,40.)); +#5230 = VECTOR('',#5231,1.); +#5231 = DIRECTION('',(1.,0.,-0.)); +#5232 = ORIENTED_EDGE('',*,*,#5233,.F.); +#5233 = EDGE_CURVE('',#2864,#5226,#5234,.T.); +#5234 = LINE('',#5235,#5236); +#5235 = CARTESIAN_POINT('',(-91.94444444444,17.8,40.)); +#5236 = VECTOR('',#5237,1.); +#5237 = DIRECTION('',(-0.,1.,0.)); +#5238 = ORIENTED_EDGE('',*,*,#2871,.T.); +#5239 = ORIENTED_EDGE('',*,*,#5201,.T.); +#5240 = PLANE('',#5241); +#5241 = AXIS2_PLACEMENT_3D('',#5242,#5243,#5244); +#5242 = CARTESIAN_POINT('',(-100.,18.,40.)); +#5243 = DIRECTION('',(0.,0.,1.)); +#5244 = DIRECTION('',(1.,0.,-0.)); +#5245 = ADVANCED_FACE('',(#5246),#5264,.F.); +#5246 = FACE_BOUND('',#5247,.F.); +#5247 = EDGE_LOOP('',(#5248,#5249,#5250,#5258)); +#5248 = ORIENTED_EDGE('',*,*,#5233,.F.); +#5249 = ORIENTED_EDGE('',*,*,#2863,.T.); +#5250 = ORIENTED_EDGE('',*,*,#5251,.T.); +#5251 = EDGE_CURVE('',#2856,#5252,#5254,.T.); +#5252 = VERTEX_POINT('',#5253); +#5253 = CARTESIAN_POINT('',(-91.94444444444,20.,8.)); +#5254 = LINE('',#5255,#5256); +#5255 = CARTESIAN_POINT('',(-91.94444444444,17.8,8.)); +#5256 = VECTOR('',#5257,1.); +#5257 = DIRECTION('',(-0.,1.,0.)); +#5258 = ORIENTED_EDGE('',*,*,#5259,.F.); +#5259 = EDGE_CURVE('',#5226,#5252,#5260,.T.); +#5260 = LINE('',#5261,#5262); +#5261 = CARTESIAN_POINT('',(-91.94444444444,20.,4.)); +#5262 = VECTOR('',#5263,1.); +#5263 = DIRECTION('',(-0.,0.,-1.)); +#5264 = PLANE('',#5265); +#5265 = AXIS2_PLACEMENT_3D('',#5266,#5267,#5268); +#5266 = CARTESIAN_POINT('',(-91.94444444444,17.8,8.)); +#5267 = DIRECTION('',(1.,0.,-0.)); +#5268 = DIRECTION('',(0.,0.,1.)); +#5269 = ADVANCED_FACE('',(#5270),#5288,.T.); +#5270 = FACE_BOUND('',#5271,.T.); +#5271 = EDGE_LOOP('',(#5272,#5280,#5281,#5282)); #5272 = ORIENTED_EDGE('',*,*,#5273,.F.); -#5273 = EDGE_CURVE('',#5274,#5276,#5278,.T.); +#5273 = EDGE_CURVE('',#2848,#5274,#5276,.T.); #5274 = VERTEX_POINT('',#5275); -#5275 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5276 = VERTEX_POINT('',#5277); -#5277 = CARTESIAN_POINT('',(47.5,17.9,40.)); -#5278 = LINE('',#5279,#5280); -#5279 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5280 = VECTOR('',#5281,1.); -#5281 = DIRECTION('',(0.,0.,1.)); -#5282 = ORIENTED_EDGE('',*,*,#5283,.T.); -#5283 = EDGE_CURVE('',#5274,#5284,#5286,.T.); -#5284 = VERTEX_POINT('',#5285); -#5285 = CARTESIAN_POINT('',(47.5,20.1,8.)); -#5286 = LINE('',#5287,#5288); -#5287 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5288 = VECTOR('',#5289,1.); -#5289 = DIRECTION('',(-0.,1.,0.)); -#5290 = ORIENTED_EDGE('',*,*,#5291,.T.); -#5291 = EDGE_CURVE('',#5284,#5292,#5294,.T.); -#5292 = VERTEX_POINT('',#5293); -#5293 = CARTESIAN_POINT('',(47.5,20.1,40.)); -#5294 = LINE('',#5295,#5296); -#5295 = CARTESIAN_POINT('',(47.5,20.1,8.)); -#5296 = VECTOR('',#5297,1.); -#5297 = DIRECTION('',(0.,0.,1.)); -#5298 = ORIENTED_EDGE('',*,*,#5299,.F.); -#5299 = EDGE_CURVE('',#5276,#5292,#5300,.T.); +#5275 = CARTESIAN_POINT('',(-96.94444444444,20.,8.)); +#5276 = LINE('',#5277,#5278); +#5277 = CARTESIAN_POINT('',(-96.94444444444,17.8,8.)); +#5278 = VECTOR('',#5279,1.); +#5279 = DIRECTION('',(-0.,1.,0.)); +#5280 = ORIENTED_EDGE('',*,*,#2855,.T.); +#5281 = ORIENTED_EDGE('',*,*,#5251,.T.); +#5282 = ORIENTED_EDGE('',*,*,#5283,.F.); +#5283 = EDGE_CURVE('',#5274,#5252,#5284,.T.); +#5284 = LINE('',#5285,#5286); +#5285 = CARTESIAN_POINT('',(-98.47222222222,20.,8.)); +#5286 = VECTOR('',#5287,1.); +#5287 = DIRECTION('',(1.,0.,-0.)); +#5288 = PLANE('',#5289); +#5289 = AXIS2_PLACEMENT_3D('',#5290,#5291,#5292); +#5290 = CARTESIAN_POINT('',(-96.94444444444,17.8,8.)); +#5291 = DIRECTION('',(0.,0.,1.)); +#5292 = DIRECTION('',(1.,0.,-0.)); +#5293 = ADVANCED_FACE('',(#5294),#5312,.T.); +#5294 = FACE_BOUND('',#5295,.T.); +#5295 = EDGE_LOOP('',(#5296,#5304,#5305,#5306)); +#5296 = ORIENTED_EDGE('',*,*,#5297,.F.); +#5297 = EDGE_CURVE('',#2840,#5298,#5300,.T.); +#5298 = VERTEX_POINT('',#5299); +#5299 = CARTESIAN_POINT('',(-96.94444444444,20.,40.)); #5300 = LINE('',#5301,#5302); -#5301 = CARTESIAN_POINT('',(47.5,17.9,40.)); +#5301 = CARTESIAN_POINT('',(-96.94444444444,17.8,40.)); #5302 = VECTOR('',#5303,1.); #5303 = DIRECTION('',(-0.,1.,0.)); -#5304 = PLANE('',#5305); -#5305 = AXIS2_PLACEMENT_3D('',#5306,#5307,#5308); -#5306 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5307 = DIRECTION('',(1.,0.,-0.)); -#5308 = DIRECTION('',(0.,0.,1.)); -#5309 = ADVANCED_FACE('',(#5310),#5344,.T.); -#5310 = FACE_BOUND('',#5311,.T.); -#5311 = EDGE_LOOP('',(#5312,#5322,#5330,#5338)); -#5312 = ORIENTED_EDGE('',*,*,#5313,.F.); -#5313 = EDGE_CURVE('',#5314,#5316,#5318,.T.); -#5314 = VERTEX_POINT('',#5315); -#5315 = CARTESIAN_POINT('',(52.5,17.9,8.)); -#5316 = VERTEX_POINT('',#5317); -#5317 = CARTESIAN_POINT('',(52.5,17.9,40.)); -#5318 = LINE('',#5319,#5320); -#5319 = CARTESIAN_POINT('',(52.5,17.9,8.)); -#5320 = VECTOR('',#5321,1.); -#5321 = DIRECTION('',(0.,0.,1.)); -#5322 = ORIENTED_EDGE('',*,*,#5323,.T.); -#5323 = EDGE_CURVE('',#5314,#5324,#5326,.T.); -#5324 = VERTEX_POINT('',#5325); -#5325 = CARTESIAN_POINT('',(52.5,20.1,8.)); -#5326 = LINE('',#5327,#5328); -#5327 = CARTESIAN_POINT('',(52.5,17.9,8.)); -#5328 = VECTOR('',#5329,1.); -#5329 = DIRECTION('',(-0.,1.,0.)); -#5330 = ORIENTED_EDGE('',*,*,#5331,.T.); -#5331 = EDGE_CURVE('',#5324,#5332,#5334,.T.); -#5332 = VERTEX_POINT('',#5333); -#5333 = CARTESIAN_POINT('',(52.5,20.1,40.)); -#5334 = LINE('',#5335,#5336); -#5335 = CARTESIAN_POINT('',(52.5,20.1,8.)); -#5336 = VECTOR('',#5337,1.); -#5337 = DIRECTION('',(0.,0.,1.)); -#5338 = ORIENTED_EDGE('',*,*,#5339,.F.); -#5339 = EDGE_CURVE('',#5316,#5332,#5340,.T.); -#5340 = LINE('',#5341,#5342); -#5341 = CARTESIAN_POINT('',(52.5,17.9,40.)); -#5342 = VECTOR('',#5343,1.); -#5343 = DIRECTION('',(-0.,1.,0.)); -#5344 = PLANE('',#5345); -#5345 = AXIS2_PLACEMENT_3D('',#5346,#5347,#5348); -#5346 = CARTESIAN_POINT('',(52.5,17.9,8.)); -#5347 = DIRECTION('',(1.,0.,-0.)); -#5348 = DIRECTION('',(0.,0.,1.)); -#5349 = ADVANCED_FACE('',(#5350),#5366,.F.); -#5350 = FACE_BOUND('',#5351,.F.); -#5351 = EDGE_LOOP('',(#5352,#5358,#5359,#5365)); -#5352 = ORIENTED_EDGE('',*,*,#5353,.F.); -#5353 = EDGE_CURVE('',#5274,#5314,#5354,.T.); -#5354 = LINE('',#5355,#5356); -#5355 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5356 = VECTOR('',#5357,1.); -#5357 = DIRECTION('',(1.,0.,-0.)); -#5358 = ORIENTED_EDGE('',*,*,#5273,.T.); -#5359 = ORIENTED_EDGE('',*,*,#5360,.T.); -#5360 = EDGE_CURVE('',#5276,#5316,#5361,.T.); -#5361 = LINE('',#5362,#5363); -#5362 = CARTESIAN_POINT('',(47.5,17.9,40.)); -#5363 = VECTOR('',#5364,1.); -#5364 = DIRECTION('',(1.,0.,-0.)); -#5365 = ORIENTED_EDGE('',*,*,#5313,.F.); -#5366 = PLANE('',#5367); -#5367 = AXIS2_PLACEMENT_3D('',#5368,#5369,#5370); -#5368 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5369 = DIRECTION('',(-0.,1.,0.)); -#5370 = DIRECTION('',(0.,0.,1.)); -#5371 = ADVANCED_FACE('',(#5372),#5388,.T.); -#5372 = FACE_BOUND('',#5373,.T.); -#5373 = EDGE_LOOP('',(#5374,#5380,#5381,#5387)); -#5374 = ORIENTED_EDGE('',*,*,#5375,.F.); -#5375 = EDGE_CURVE('',#5284,#5324,#5376,.T.); -#5376 = LINE('',#5377,#5378); -#5377 = CARTESIAN_POINT('',(47.5,20.1,8.)); -#5378 = VECTOR('',#5379,1.); -#5379 = DIRECTION('',(1.,0.,-0.)); -#5380 = ORIENTED_EDGE('',*,*,#5291,.T.); -#5381 = ORIENTED_EDGE('',*,*,#5382,.T.); -#5382 = EDGE_CURVE('',#5292,#5332,#5383,.T.); -#5383 = LINE('',#5384,#5385); -#5384 = CARTESIAN_POINT('',(47.5,20.1,40.)); -#5385 = VECTOR('',#5386,1.); -#5386 = DIRECTION('',(1.,0.,-0.)); -#5387 = ORIENTED_EDGE('',*,*,#5331,.F.); -#5388 = PLANE('',#5389); -#5389 = AXIS2_PLACEMENT_3D('',#5390,#5391,#5392); -#5390 = CARTESIAN_POINT('',(47.5,20.1,8.)); -#5391 = DIRECTION('',(-0.,1.,0.)); -#5392 = DIRECTION('',(0.,0.,1.)); -#5393 = ADVANCED_FACE('',(#5394),#5400,.F.); -#5394 = FACE_BOUND('',#5395,.F.); -#5395 = EDGE_LOOP('',(#5396,#5397,#5398,#5399)); -#5396 = ORIENTED_EDGE('',*,*,#5283,.F.); -#5397 = ORIENTED_EDGE('',*,*,#5353,.T.); -#5398 = ORIENTED_EDGE('',*,*,#5323,.T.); -#5399 = ORIENTED_EDGE('',*,*,#5375,.F.); -#5400 = PLANE('',#5401); -#5401 = AXIS2_PLACEMENT_3D('',#5402,#5403,#5404); -#5402 = CARTESIAN_POINT('',(47.5,17.9,8.)); -#5403 = DIRECTION('',(0.,0.,1.)); -#5404 = DIRECTION('',(1.,0.,-0.)); -#5405 = ADVANCED_FACE('',(#5406),#5412,.T.); -#5406 = FACE_BOUND('',#5407,.T.); -#5407 = EDGE_LOOP('',(#5408,#5409,#5410,#5411)); -#5408 = ORIENTED_EDGE('',*,*,#5299,.F.); -#5409 = ORIENTED_EDGE('',*,*,#5360,.T.); -#5410 = ORIENTED_EDGE('',*,*,#5339,.T.); -#5411 = ORIENTED_EDGE('',*,*,#5382,.F.); -#5412 = PLANE('',#5413); -#5413 = AXIS2_PLACEMENT_3D('',#5414,#5415,#5416); -#5414 = CARTESIAN_POINT('',(47.5,17.9,40.)); -#5415 = DIRECTION('',(0.,0.,1.)); -#5416 = DIRECTION('',(1.,0.,-0.)); -#5417 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5421)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#5418,#5419,#5420)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#5418 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#5419 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#5420 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#5421 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5418, - 'distance_accuracy_value','confusion accuracy'); -#5422 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5423,#5425); -#5423 = ( REPRESENTATION_RELATIONSHIP('','',#5266,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5424) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#5424 = ITEM_DEFINED_TRANSFORMATION('','',#11,#135); -#5425 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #5426); -#5426 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('31','WireDuct_RightCombSlot_14', - '',#5,#5261,$); -#5427 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5263)); -#5428 = SHAPE_DEFINITION_REPRESENTATION(#5429,#5435); -#5429 = PRODUCT_DEFINITION_SHAPE('','',#5430); -#5430 = PRODUCT_DEFINITION('design','',#5431,#5434); -#5431 = PRODUCT_DEFINITION_FORMATION('','',#5432); -#5432 = PRODUCT('WireDuct_LeftCombSlot_15','WireDuct_LeftCombSlot_15','' - ,(#5433)); -#5433 = PRODUCT_CONTEXT('',#2,'mechanical'); -#5434 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#5435 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5436),#5586); -#5436 = MANIFOLD_SOLID_BREP('',#5437); -#5437 = CLOSED_SHELL('',(#5438,#5478,#5518,#5540,#5562,#5574)); -#5438 = ADVANCED_FACE('',(#5439),#5473,.F.); -#5439 = FACE_BOUND('',#5440,.F.); -#5440 = EDGE_LOOP('',(#5441,#5451,#5459,#5467)); -#5441 = ORIENTED_EDGE('',*,*,#5442,.F.); -#5442 = EDGE_CURVE('',#5443,#5445,#5447,.T.); -#5443 = VERTEX_POINT('',#5444); -#5444 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5445 = VERTEX_POINT('',#5446); -#5446 = CARTESIAN_POINT('',(58.611111111111,-20.1,40.)); -#5447 = LINE('',#5448,#5449); -#5448 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5449 = VECTOR('',#5450,1.); -#5450 = DIRECTION('',(0.,0.,1.)); -#5451 = ORIENTED_EDGE('',*,*,#5452,.T.); -#5452 = EDGE_CURVE('',#5443,#5453,#5455,.T.); -#5453 = VERTEX_POINT('',#5454); -#5454 = CARTESIAN_POINT('',(58.611111111111,-17.9,8.)); -#5455 = LINE('',#5456,#5457); -#5456 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5457 = VECTOR('',#5458,1.); -#5458 = DIRECTION('',(-0.,1.,0.)); -#5459 = ORIENTED_EDGE('',*,*,#5460,.T.); -#5460 = EDGE_CURVE('',#5453,#5461,#5463,.T.); -#5461 = VERTEX_POINT('',#5462); -#5462 = CARTESIAN_POINT('',(58.611111111111,-17.9,40.)); -#5463 = LINE('',#5464,#5465); -#5464 = CARTESIAN_POINT('',(58.611111111111,-17.9,8.)); -#5465 = VECTOR('',#5466,1.); -#5466 = DIRECTION('',(0.,0.,1.)); -#5467 = ORIENTED_EDGE('',*,*,#5468,.F.); -#5468 = EDGE_CURVE('',#5445,#5461,#5469,.T.); -#5469 = LINE('',#5470,#5471); -#5470 = CARTESIAN_POINT('',(58.611111111111,-20.1,40.)); -#5471 = VECTOR('',#5472,1.); -#5472 = DIRECTION('',(-0.,1.,0.)); -#5473 = PLANE('',#5474); -#5474 = AXIS2_PLACEMENT_3D('',#5475,#5476,#5477); -#5475 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5476 = DIRECTION('',(1.,0.,-0.)); -#5477 = DIRECTION('',(0.,0.,1.)); -#5478 = ADVANCED_FACE('',(#5479),#5513,.T.); -#5479 = FACE_BOUND('',#5480,.T.); -#5480 = EDGE_LOOP('',(#5481,#5491,#5499,#5507)); -#5481 = ORIENTED_EDGE('',*,*,#5482,.F.); -#5482 = EDGE_CURVE('',#5483,#5485,#5487,.T.); -#5483 = VERTEX_POINT('',#5484); -#5484 = CARTESIAN_POINT('',(63.611111111111,-20.1,8.)); -#5485 = VERTEX_POINT('',#5486); -#5486 = CARTESIAN_POINT('',(63.611111111111,-20.1,40.)); -#5487 = LINE('',#5488,#5489); -#5488 = CARTESIAN_POINT('',(63.611111111111,-20.1,8.)); -#5489 = VECTOR('',#5490,1.); -#5490 = DIRECTION('',(0.,0.,1.)); -#5491 = ORIENTED_EDGE('',*,*,#5492,.T.); -#5492 = EDGE_CURVE('',#5483,#5493,#5495,.T.); -#5493 = VERTEX_POINT('',#5494); -#5494 = CARTESIAN_POINT('',(63.611111111111,-17.9,8.)); -#5495 = LINE('',#5496,#5497); -#5496 = CARTESIAN_POINT('',(63.611111111111,-20.1,8.)); -#5497 = VECTOR('',#5498,1.); -#5498 = DIRECTION('',(-0.,1.,0.)); -#5499 = ORIENTED_EDGE('',*,*,#5500,.T.); -#5500 = EDGE_CURVE('',#5493,#5501,#5503,.T.); -#5501 = VERTEX_POINT('',#5502); -#5502 = CARTESIAN_POINT('',(63.611111111111,-17.9,40.)); -#5503 = LINE('',#5504,#5505); -#5504 = CARTESIAN_POINT('',(63.611111111111,-17.9,8.)); -#5505 = VECTOR('',#5506,1.); -#5506 = DIRECTION('',(0.,0.,1.)); -#5507 = ORIENTED_EDGE('',*,*,#5508,.F.); -#5508 = EDGE_CURVE('',#5485,#5501,#5509,.T.); -#5509 = LINE('',#5510,#5511); -#5510 = CARTESIAN_POINT('',(63.611111111111,-20.1,40.)); -#5511 = VECTOR('',#5512,1.); -#5512 = DIRECTION('',(-0.,1.,0.)); -#5513 = PLANE('',#5514); -#5514 = AXIS2_PLACEMENT_3D('',#5515,#5516,#5517); -#5515 = CARTESIAN_POINT('',(63.611111111111,-20.1,8.)); -#5516 = DIRECTION('',(1.,0.,-0.)); -#5517 = DIRECTION('',(0.,0.,1.)); -#5518 = ADVANCED_FACE('',(#5519),#5535,.F.); -#5519 = FACE_BOUND('',#5520,.F.); -#5520 = EDGE_LOOP('',(#5521,#5527,#5528,#5534)); -#5521 = ORIENTED_EDGE('',*,*,#5522,.F.); -#5522 = EDGE_CURVE('',#5443,#5483,#5523,.T.); -#5523 = LINE('',#5524,#5525); -#5524 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5525 = VECTOR('',#5526,1.); -#5526 = DIRECTION('',(1.,0.,-0.)); -#5527 = ORIENTED_EDGE('',*,*,#5442,.T.); -#5528 = ORIENTED_EDGE('',*,*,#5529,.T.); -#5529 = EDGE_CURVE('',#5445,#5485,#5530,.T.); -#5530 = LINE('',#5531,#5532); -#5531 = CARTESIAN_POINT('',(58.611111111111,-20.1,40.)); -#5532 = VECTOR('',#5533,1.); -#5533 = DIRECTION('',(1.,0.,-0.)); -#5534 = ORIENTED_EDGE('',*,*,#5482,.F.); -#5535 = PLANE('',#5536); -#5536 = AXIS2_PLACEMENT_3D('',#5537,#5538,#5539); -#5537 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5538 = DIRECTION('',(-0.,1.,0.)); -#5539 = DIRECTION('',(0.,0.,1.)); -#5540 = ADVANCED_FACE('',(#5541),#5557,.T.); -#5541 = FACE_BOUND('',#5542,.T.); -#5542 = EDGE_LOOP('',(#5543,#5549,#5550,#5556)); -#5543 = ORIENTED_EDGE('',*,*,#5544,.F.); -#5544 = EDGE_CURVE('',#5453,#5493,#5545,.T.); -#5545 = LINE('',#5546,#5547); -#5546 = CARTESIAN_POINT('',(58.611111111111,-17.9,8.)); -#5547 = VECTOR('',#5548,1.); -#5548 = DIRECTION('',(1.,0.,-0.)); -#5549 = ORIENTED_EDGE('',*,*,#5460,.T.); -#5550 = ORIENTED_EDGE('',*,*,#5551,.T.); -#5551 = EDGE_CURVE('',#5461,#5501,#5552,.T.); -#5552 = LINE('',#5553,#5554); -#5553 = CARTESIAN_POINT('',(58.611111111111,-17.9,40.)); -#5554 = VECTOR('',#5555,1.); -#5555 = DIRECTION('',(1.,0.,-0.)); -#5556 = ORIENTED_EDGE('',*,*,#5500,.F.); -#5557 = PLANE('',#5558); -#5558 = AXIS2_PLACEMENT_3D('',#5559,#5560,#5561); -#5559 = CARTESIAN_POINT('',(58.611111111111,-17.9,8.)); -#5560 = DIRECTION('',(-0.,1.,0.)); -#5561 = DIRECTION('',(0.,0.,1.)); -#5562 = ADVANCED_FACE('',(#5563),#5569,.F.); -#5563 = FACE_BOUND('',#5564,.F.); -#5564 = EDGE_LOOP('',(#5565,#5566,#5567,#5568)); -#5565 = ORIENTED_EDGE('',*,*,#5452,.F.); -#5566 = ORIENTED_EDGE('',*,*,#5522,.T.); -#5567 = ORIENTED_EDGE('',*,*,#5492,.T.); -#5568 = ORIENTED_EDGE('',*,*,#5544,.F.); -#5569 = PLANE('',#5570); -#5570 = AXIS2_PLACEMENT_3D('',#5571,#5572,#5573); -#5571 = CARTESIAN_POINT('',(58.611111111111,-20.1,8.)); -#5572 = DIRECTION('',(0.,0.,1.)); -#5573 = DIRECTION('',(1.,0.,-0.)); -#5574 = ADVANCED_FACE('',(#5575),#5581,.T.); -#5575 = FACE_BOUND('',#5576,.T.); -#5576 = EDGE_LOOP('',(#5577,#5578,#5579,#5580)); -#5577 = ORIENTED_EDGE('',*,*,#5468,.F.); -#5578 = ORIENTED_EDGE('',*,*,#5529,.T.); -#5579 = ORIENTED_EDGE('',*,*,#5508,.T.); -#5580 = ORIENTED_EDGE('',*,*,#5551,.F.); -#5581 = PLANE('',#5582); -#5582 = AXIS2_PLACEMENT_3D('',#5583,#5584,#5585); -#5583 = CARTESIAN_POINT('',(58.611111111111,-20.1,40.)); -#5584 = DIRECTION('',(0.,0.,1.)); -#5585 = DIRECTION('',(1.,0.,-0.)); -#5586 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5590)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#5587,#5588,#5589)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#5587 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#5588 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#5589 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#5590 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5587, - 'distance_accuracy_value','confusion accuracy'); -#5591 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5592,#5594); -#5592 = ( REPRESENTATION_RELATIONSHIP('','',#5435,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5593) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#5593 = ITEM_DEFINED_TRANSFORMATION('','',#11,#139); -#5594 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #5595); -#5595 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('32','WireDuct_LeftCombSlot_15', - '',#5,#5430,$); -#5596 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5432)); -#5597 = SHAPE_DEFINITION_REPRESENTATION(#5598,#5604); -#5598 = PRODUCT_DEFINITION_SHAPE('','',#5599); -#5599 = PRODUCT_DEFINITION('design','',#5600,#5603); -#5600 = PRODUCT_DEFINITION_FORMATION('','',#5601); -#5601 = PRODUCT('WireDuct_RightCombSlot_15','WireDuct_RightCombSlot_15', - '',(#5602)); -#5602 = PRODUCT_CONTEXT('',#2,'mechanical'); -#5603 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#5604 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5605),#5755); -#5605 = MANIFOLD_SOLID_BREP('',#5606); -#5606 = CLOSED_SHELL('',(#5607,#5647,#5687,#5709,#5731,#5743)); -#5607 = ADVANCED_FACE('',(#5608),#5642,.F.); -#5608 = FACE_BOUND('',#5609,.F.); -#5609 = EDGE_LOOP('',(#5610,#5620,#5628,#5636)); -#5610 = ORIENTED_EDGE('',*,*,#5611,.F.); -#5611 = EDGE_CURVE('',#5612,#5614,#5616,.T.); -#5612 = VERTEX_POINT('',#5613); -#5613 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5614 = VERTEX_POINT('',#5615); -#5615 = CARTESIAN_POINT('',(58.611111111111,17.9,40.)); -#5616 = LINE('',#5617,#5618); -#5617 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5618 = VECTOR('',#5619,1.); -#5619 = DIRECTION('',(0.,0.,1.)); -#5620 = ORIENTED_EDGE('',*,*,#5621,.T.); -#5621 = EDGE_CURVE('',#5612,#5622,#5624,.T.); -#5622 = VERTEX_POINT('',#5623); -#5623 = CARTESIAN_POINT('',(58.611111111111,20.1,8.)); -#5624 = LINE('',#5625,#5626); -#5625 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5626 = VECTOR('',#5627,1.); -#5627 = DIRECTION('',(-0.,1.,0.)); -#5628 = ORIENTED_EDGE('',*,*,#5629,.T.); -#5629 = EDGE_CURVE('',#5622,#5630,#5632,.T.); -#5630 = VERTEX_POINT('',#5631); -#5631 = CARTESIAN_POINT('',(58.611111111111,20.1,40.)); -#5632 = LINE('',#5633,#5634); -#5633 = CARTESIAN_POINT('',(58.611111111111,20.1,8.)); -#5634 = VECTOR('',#5635,1.); -#5635 = DIRECTION('',(0.,0.,1.)); -#5636 = ORIENTED_EDGE('',*,*,#5637,.F.); -#5637 = EDGE_CURVE('',#5614,#5630,#5638,.T.); -#5638 = LINE('',#5639,#5640); -#5639 = CARTESIAN_POINT('',(58.611111111111,17.9,40.)); -#5640 = VECTOR('',#5641,1.); -#5641 = DIRECTION('',(-0.,1.,0.)); -#5642 = PLANE('',#5643); -#5643 = AXIS2_PLACEMENT_3D('',#5644,#5645,#5646); -#5644 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5645 = DIRECTION('',(1.,0.,-0.)); -#5646 = DIRECTION('',(0.,0.,1.)); -#5647 = ADVANCED_FACE('',(#5648),#5682,.T.); -#5648 = FACE_BOUND('',#5649,.T.); -#5649 = EDGE_LOOP('',(#5650,#5660,#5668,#5676)); -#5650 = ORIENTED_EDGE('',*,*,#5651,.F.); -#5651 = EDGE_CURVE('',#5652,#5654,#5656,.T.); -#5652 = VERTEX_POINT('',#5653); -#5653 = CARTESIAN_POINT('',(63.611111111111,17.9,8.)); -#5654 = VERTEX_POINT('',#5655); -#5655 = CARTESIAN_POINT('',(63.611111111111,17.9,40.)); -#5656 = LINE('',#5657,#5658); -#5657 = CARTESIAN_POINT('',(63.611111111111,17.9,8.)); -#5658 = VECTOR('',#5659,1.); -#5659 = DIRECTION('',(0.,0.,1.)); -#5660 = ORIENTED_EDGE('',*,*,#5661,.T.); -#5661 = EDGE_CURVE('',#5652,#5662,#5664,.T.); -#5662 = VERTEX_POINT('',#5663); -#5663 = CARTESIAN_POINT('',(63.611111111111,20.1,8.)); -#5664 = LINE('',#5665,#5666); -#5665 = CARTESIAN_POINT('',(63.611111111111,17.9,8.)); -#5666 = VECTOR('',#5667,1.); -#5667 = DIRECTION('',(-0.,1.,0.)); -#5668 = ORIENTED_EDGE('',*,*,#5669,.T.); -#5669 = EDGE_CURVE('',#5662,#5670,#5672,.T.); -#5670 = VERTEX_POINT('',#5671); -#5671 = CARTESIAN_POINT('',(63.611111111111,20.1,40.)); -#5672 = LINE('',#5673,#5674); -#5673 = CARTESIAN_POINT('',(63.611111111111,20.1,8.)); -#5674 = VECTOR('',#5675,1.); -#5675 = DIRECTION('',(0.,0.,1.)); -#5676 = ORIENTED_EDGE('',*,*,#5677,.F.); -#5677 = EDGE_CURVE('',#5654,#5670,#5678,.T.); -#5678 = LINE('',#5679,#5680); -#5679 = CARTESIAN_POINT('',(63.611111111111,17.9,40.)); -#5680 = VECTOR('',#5681,1.); -#5681 = DIRECTION('',(-0.,1.,0.)); -#5682 = PLANE('',#5683); -#5683 = AXIS2_PLACEMENT_3D('',#5684,#5685,#5686); -#5684 = CARTESIAN_POINT('',(63.611111111111,17.9,8.)); -#5685 = DIRECTION('',(1.,0.,-0.)); -#5686 = DIRECTION('',(0.,0.,1.)); -#5687 = ADVANCED_FACE('',(#5688),#5704,.F.); -#5688 = FACE_BOUND('',#5689,.F.); -#5689 = EDGE_LOOP('',(#5690,#5696,#5697,#5703)); -#5690 = ORIENTED_EDGE('',*,*,#5691,.F.); -#5691 = EDGE_CURVE('',#5612,#5652,#5692,.T.); -#5692 = LINE('',#5693,#5694); -#5693 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5694 = VECTOR('',#5695,1.); -#5695 = DIRECTION('',(1.,0.,-0.)); -#5696 = ORIENTED_EDGE('',*,*,#5611,.T.); -#5697 = ORIENTED_EDGE('',*,*,#5698,.T.); -#5698 = EDGE_CURVE('',#5614,#5654,#5699,.T.); -#5699 = LINE('',#5700,#5701); -#5700 = CARTESIAN_POINT('',(58.611111111111,17.9,40.)); -#5701 = VECTOR('',#5702,1.); -#5702 = DIRECTION('',(1.,0.,-0.)); -#5703 = ORIENTED_EDGE('',*,*,#5651,.F.); -#5704 = PLANE('',#5705); -#5705 = AXIS2_PLACEMENT_3D('',#5706,#5707,#5708); -#5706 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5707 = DIRECTION('',(-0.,1.,0.)); -#5708 = DIRECTION('',(0.,0.,1.)); -#5709 = ADVANCED_FACE('',(#5710),#5726,.T.); -#5710 = FACE_BOUND('',#5711,.T.); -#5711 = EDGE_LOOP('',(#5712,#5718,#5719,#5725)); -#5712 = ORIENTED_EDGE('',*,*,#5713,.F.); -#5713 = EDGE_CURVE('',#5622,#5662,#5714,.T.); -#5714 = LINE('',#5715,#5716); -#5715 = CARTESIAN_POINT('',(58.611111111111,20.1,8.)); -#5716 = VECTOR('',#5717,1.); -#5717 = DIRECTION('',(1.,0.,-0.)); -#5718 = ORIENTED_EDGE('',*,*,#5629,.T.); -#5719 = ORIENTED_EDGE('',*,*,#5720,.T.); -#5720 = EDGE_CURVE('',#5630,#5670,#5721,.T.); -#5721 = LINE('',#5722,#5723); -#5722 = CARTESIAN_POINT('',(58.611111111111,20.1,40.)); -#5723 = VECTOR('',#5724,1.); -#5724 = DIRECTION('',(1.,0.,-0.)); -#5725 = ORIENTED_EDGE('',*,*,#5669,.F.); -#5726 = PLANE('',#5727); -#5727 = AXIS2_PLACEMENT_3D('',#5728,#5729,#5730); -#5728 = CARTESIAN_POINT('',(58.611111111111,20.1,8.)); -#5729 = DIRECTION('',(-0.,1.,0.)); -#5730 = DIRECTION('',(0.,0.,1.)); -#5731 = ADVANCED_FACE('',(#5732),#5738,.F.); -#5732 = FACE_BOUND('',#5733,.F.); -#5733 = EDGE_LOOP('',(#5734,#5735,#5736,#5737)); -#5734 = ORIENTED_EDGE('',*,*,#5621,.F.); -#5735 = ORIENTED_EDGE('',*,*,#5691,.T.); -#5736 = ORIENTED_EDGE('',*,*,#5661,.T.); -#5737 = ORIENTED_EDGE('',*,*,#5713,.F.); -#5738 = PLANE('',#5739); -#5739 = AXIS2_PLACEMENT_3D('',#5740,#5741,#5742); -#5740 = CARTESIAN_POINT('',(58.611111111111,17.9,8.)); -#5741 = DIRECTION('',(0.,0.,1.)); -#5742 = DIRECTION('',(1.,0.,-0.)); -#5743 = ADVANCED_FACE('',(#5744),#5750,.T.); -#5744 = FACE_BOUND('',#5745,.T.); -#5745 = EDGE_LOOP('',(#5746,#5747,#5748,#5749)); -#5746 = ORIENTED_EDGE('',*,*,#5637,.F.); -#5747 = ORIENTED_EDGE('',*,*,#5698,.T.); -#5748 = ORIENTED_EDGE('',*,*,#5677,.T.); -#5749 = ORIENTED_EDGE('',*,*,#5720,.F.); -#5750 = PLANE('',#5751); -#5751 = AXIS2_PLACEMENT_3D('',#5752,#5753,#5754); -#5752 = CARTESIAN_POINT('',(58.611111111111,17.9,40.)); -#5753 = DIRECTION('',(0.,0.,1.)); -#5754 = DIRECTION('',(1.,0.,-0.)); -#5755 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5759)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#5756,#5757,#5758)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#5756 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#5757 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#5758 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#5759 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5756, - 'distance_accuracy_value','confusion accuracy'); -#5760 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5761,#5763); -#5761 = ( REPRESENTATION_RELATIONSHIP('','',#5604,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5762) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#5762 = ITEM_DEFINED_TRANSFORMATION('','',#11,#143); -#5763 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #5764); -#5764 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('33','WireDuct_RightCombSlot_15', - '',#5,#5599,$); -#5765 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5601)); -#5766 = SHAPE_DEFINITION_REPRESENTATION(#5767,#5773); -#5767 = PRODUCT_DEFINITION_SHAPE('','',#5768); -#5768 = PRODUCT_DEFINITION('design','',#5769,#5772); -#5769 = PRODUCT_DEFINITION_FORMATION('','',#5770); -#5770 = PRODUCT('WireDuct_LeftCombSlot_16','WireDuct_LeftCombSlot_16','' - ,(#5771)); -#5771 = PRODUCT_CONTEXT('',#2,'mechanical'); -#5772 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#5773 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5774),#5924); -#5774 = MANIFOLD_SOLID_BREP('',#5775); -#5775 = CLOSED_SHELL('',(#5776,#5816,#5856,#5878,#5900,#5912)); -#5776 = ADVANCED_FACE('',(#5777),#5811,.F.); -#5777 = FACE_BOUND('',#5778,.F.); -#5778 = EDGE_LOOP('',(#5779,#5789,#5797,#5805)); -#5779 = ORIENTED_EDGE('',*,*,#5780,.F.); -#5780 = EDGE_CURVE('',#5781,#5783,#5785,.T.); -#5781 = VERTEX_POINT('',#5782); -#5782 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5783 = VERTEX_POINT('',#5784); -#5784 = CARTESIAN_POINT('',(69.722222222222,-20.1,40.)); -#5785 = LINE('',#5786,#5787); -#5786 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5787 = VECTOR('',#5788,1.); -#5788 = DIRECTION('',(0.,0.,1.)); -#5789 = ORIENTED_EDGE('',*,*,#5790,.T.); -#5790 = EDGE_CURVE('',#5781,#5791,#5793,.T.); -#5791 = VERTEX_POINT('',#5792); -#5792 = CARTESIAN_POINT('',(69.722222222222,-17.9,8.)); -#5793 = LINE('',#5794,#5795); -#5794 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5795 = VECTOR('',#5796,1.); -#5796 = DIRECTION('',(-0.,1.,0.)); -#5797 = ORIENTED_EDGE('',*,*,#5798,.T.); -#5798 = EDGE_CURVE('',#5791,#5799,#5801,.T.); -#5799 = VERTEX_POINT('',#5800); -#5800 = CARTESIAN_POINT('',(69.722222222222,-17.9,40.)); -#5801 = LINE('',#5802,#5803); -#5802 = CARTESIAN_POINT('',(69.722222222222,-17.9,8.)); -#5803 = VECTOR('',#5804,1.); -#5804 = DIRECTION('',(0.,0.,1.)); -#5805 = ORIENTED_EDGE('',*,*,#5806,.F.); -#5806 = EDGE_CURVE('',#5783,#5799,#5807,.T.); -#5807 = LINE('',#5808,#5809); -#5808 = CARTESIAN_POINT('',(69.722222222222,-20.1,40.)); -#5809 = VECTOR('',#5810,1.); -#5810 = DIRECTION('',(-0.,1.,0.)); -#5811 = PLANE('',#5812); -#5812 = AXIS2_PLACEMENT_3D('',#5813,#5814,#5815); -#5813 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5814 = DIRECTION('',(1.,0.,-0.)); -#5815 = DIRECTION('',(0.,0.,1.)); -#5816 = ADVANCED_FACE('',(#5817),#5851,.T.); -#5817 = FACE_BOUND('',#5818,.T.); -#5818 = EDGE_LOOP('',(#5819,#5829,#5837,#5845)); -#5819 = ORIENTED_EDGE('',*,*,#5820,.F.); -#5820 = EDGE_CURVE('',#5821,#5823,#5825,.T.); -#5821 = VERTEX_POINT('',#5822); -#5822 = CARTESIAN_POINT('',(74.722222222222,-20.1,8.)); -#5823 = VERTEX_POINT('',#5824); -#5824 = CARTESIAN_POINT('',(74.722222222222,-20.1,40.)); -#5825 = LINE('',#5826,#5827); -#5826 = CARTESIAN_POINT('',(74.722222222222,-20.1,8.)); -#5827 = VECTOR('',#5828,1.); -#5828 = DIRECTION('',(0.,0.,1.)); -#5829 = ORIENTED_EDGE('',*,*,#5830,.T.); -#5830 = EDGE_CURVE('',#5821,#5831,#5833,.T.); -#5831 = VERTEX_POINT('',#5832); -#5832 = CARTESIAN_POINT('',(74.722222222222,-17.9,8.)); -#5833 = LINE('',#5834,#5835); -#5834 = CARTESIAN_POINT('',(74.722222222222,-20.1,8.)); -#5835 = VECTOR('',#5836,1.); -#5836 = DIRECTION('',(-0.,1.,0.)); -#5837 = ORIENTED_EDGE('',*,*,#5838,.T.); -#5838 = EDGE_CURVE('',#5831,#5839,#5841,.T.); -#5839 = VERTEX_POINT('',#5840); -#5840 = CARTESIAN_POINT('',(74.722222222222,-17.9,40.)); -#5841 = LINE('',#5842,#5843); -#5842 = CARTESIAN_POINT('',(74.722222222222,-17.9,8.)); -#5843 = VECTOR('',#5844,1.); -#5844 = DIRECTION('',(0.,0.,1.)); -#5845 = ORIENTED_EDGE('',*,*,#5846,.F.); -#5846 = EDGE_CURVE('',#5823,#5839,#5847,.T.); -#5847 = LINE('',#5848,#5849); -#5848 = CARTESIAN_POINT('',(74.722222222222,-20.1,40.)); -#5849 = VECTOR('',#5850,1.); -#5850 = DIRECTION('',(-0.,1.,0.)); -#5851 = PLANE('',#5852); -#5852 = AXIS2_PLACEMENT_3D('',#5853,#5854,#5855); -#5853 = CARTESIAN_POINT('',(74.722222222222,-20.1,8.)); -#5854 = DIRECTION('',(1.,0.,-0.)); -#5855 = DIRECTION('',(0.,0.,1.)); -#5856 = ADVANCED_FACE('',(#5857),#5873,.F.); -#5857 = FACE_BOUND('',#5858,.F.); -#5858 = EDGE_LOOP('',(#5859,#5865,#5866,#5872)); -#5859 = ORIENTED_EDGE('',*,*,#5860,.F.); -#5860 = EDGE_CURVE('',#5781,#5821,#5861,.T.); -#5861 = LINE('',#5862,#5863); -#5862 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5863 = VECTOR('',#5864,1.); -#5864 = DIRECTION('',(1.,0.,-0.)); -#5865 = ORIENTED_EDGE('',*,*,#5780,.T.); -#5866 = ORIENTED_EDGE('',*,*,#5867,.T.); -#5867 = EDGE_CURVE('',#5783,#5823,#5868,.T.); -#5868 = LINE('',#5869,#5870); -#5869 = CARTESIAN_POINT('',(69.722222222222,-20.1,40.)); -#5870 = VECTOR('',#5871,1.); -#5871 = DIRECTION('',(1.,0.,-0.)); -#5872 = ORIENTED_EDGE('',*,*,#5820,.F.); -#5873 = PLANE('',#5874); -#5874 = AXIS2_PLACEMENT_3D('',#5875,#5876,#5877); -#5875 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5876 = DIRECTION('',(-0.,1.,0.)); -#5877 = DIRECTION('',(0.,0.,1.)); -#5878 = ADVANCED_FACE('',(#5879),#5895,.T.); -#5879 = FACE_BOUND('',#5880,.T.); -#5880 = EDGE_LOOP('',(#5881,#5887,#5888,#5894)); -#5881 = ORIENTED_EDGE('',*,*,#5882,.F.); -#5882 = EDGE_CURVE('',#5791,#5831,#5883,.T.); -#5883 = LINE('',#5884,#5885); -#5884 = CARTESIAN_POINT('',(69.722222222222,-17.9,8.)); -#5885 = VECTOR('',#5886,1.); -#5886 = DIRECTION('',(1.,0.,-0.)); -#5887 = ORIENTED_EDGE('',*,*,#5798,.T.); -#5888 = ORIENTED_EDGE('',*,*,#5889,.T.); -#5889 = EDGE_CURVE('',#5799,#5839,#5890,.T.); -#5890 = LINE('',#5891,#5892); -#5891 = CARTESIAN_POINT('',(69.722222222222,-17.9,40.)); -#5892 = VECTOR('',#5893,1.); -#5893 = DIRECTION('',(1.,0.,-0.)); -#5894 = ORIENTED_EDGE('',*,*,#5838,.F.); -#5895 = PLANE('',#5896); -#5896 = AXIS2_PLACEMENT_3D('',#5897,#5898,#5899); -#5897 = CARTESIAN_POINT('',(69.722222222222,-17.9,8.)); -#5898 = DIRECTION('',(-0.,1.,0.)); -#5899 = DIRECTION('',(0.,0.,1.)); -#5900 = ADVANCED_FACE('',(#5901),#5907,.F.); -#5901 = FACE_BOUND('',#5902,.F.); -#5902 = EDGE_LOOP('',(#5903,#5904,#5905,#5906)); -#5903 = ORIENTED_EDGE('',*,*,#5790,.F.); -#5904 = ORIENTED_EDGE('',*,*,#5860,.T.); -#5905 = ORIENTED_EDGE('',*,*,#5830,.T.); -#5906 = ORIENTED_EDGE('',*,*,#5882,.F.); -#5907 = PLANE('',#5908); -#5908 = AXIS2_PLACEMENT_3D('',#5909,#5910,#5911); -#5909 = CARTESIAN_POINT('',(69.722222222222,-20.1,8.)); -#5910 = DIRECTION('',(0.,0.,1.)); -#5911 = DIRECTION('',(1.,0.,-0.)); -#5912 = ADVANCED_FACE('',(#5913),#5919,.T.); -#5913 = FACE_BOUND('',#5914,.T.); -#5914 = EDGE_LOOP('',(#5915,#5916,#5917,#5918)); -#5915 = ORIENTED_EDGE('',*,*,#5806,.F.); -#5916 = ORIENTED_EDGE('',*,*,#5867,.T.); -#5917 = ORIENTED_EDGE('',*,*,#5846,.T.); -#5918 = ORIENTED_EDGE('',*,*,#5889,.F.); -#5919 = PLANE('',#5920); -#5920 = AXIS2_PLACEMENT_3D('',#5921,#5922,#5923); -#5921 = CARTESIAN_POINT('',(69.722222222222,-20.1,40.)); -#5922 = DIRECTION('',(0.,0.,1.)); -#5923 = DIRECTION('',(1.,0.,-0.)); -#5924 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5928)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#5925,#5926,#5927)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#5925 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#5926 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#5927 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#5928 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#5925, - 'distance_accuracy_value','confusion accuracy'); -#5929 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#5930,#5932); -#5930 = ( REPRESENTATION_RELATIONSHIP('','',#5773,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#5931) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#5931 = ITEM_DEFINED_TRANSFORMATION('','',#11,#147); -#5932 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #5933); -#5933 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('34','WireDuct_LeftCombSlot_16', - '',#5,#5768,$); -#5934 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5770)); -#5935 = SHAPE_DEFINITION_REPRESENTATION(#5936,#5942); -#5936 = PRODUCT_DEFINITION_SHAPE('','',#5937); -#5937 = PRODUCT_DEFINITION('design','',#5938,#5941); -#5938 = PRODUCT_DEFINITION_FORMATION('','',#5939); -#5939 = PRODUCT('WireDuct_RightCombSlot_16','WireDuct_RightCombSlot_16', - '',(#5940)); -#5940 = PRODUCT_CONTEXT('',#2,'mechanical'); -#5941 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#5942 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#5943),#6093); -#5943 = MANIFOLD_SOLID_BREP('',#5944); -#5944 = CLOSED_SHELL('',(#5945,#5985,#6025,#6047,#6069,#6081)); -#5945 = ADVANCED_FACE('',(#5946),#5980,.F.); -#5946 = FACE_BOUND('',#5947,.F.); -#5947 = EDGE_LOOP('',(#5948,#5958,#5966,#5974)); -#5948 = ORIENTED_EDGE('',*,*,#5949,.F.); -#5949 = EDGE_CURVE('',#5950,#5952,#5954,.T.); -#5950 = VERTEX_POINT('',#5951); -#5951 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#5952 = VERTEX_POINT('',#5953); -#5953 = CARTESIAN_POINT('',(69.722222222222,17.9,40.)); -#5954 = LINE('',#5955,#5956); -#5955 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#5956 = VECTOR('',#5957,1.); -#5957 = DIRECTION('',(0.,0.,1.)); -#5958 = ORIENTED_EDGE('',*,*,#5959,.T.); -#5959 = EDGE_CURVE('',#5950,#5960,#5962,.T.); -#5960 = VERTEX_POINT('',#5961); -#5961 = CARTESIAN_POINT('',(69.722222222222,20.1,8.)); -#5962 = LINE('',#5963,#5964); -#5963 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#5964 = VECTOR('',#5965,1.); -#5965 = DIRECTION('',(-0.,1.,0.)); -#5966 = ORIENTED_EDGE('',*,*,#5967,.T.); -#5967 = EDGE_CURVE('',#5960,#5968,#5970,.T.); -#5968 = VERTEX_POINT('',#5969); -#5969 = CARTESIAN_POINT('',(69.722222222222,20.1,40.)); -#5970 = LINE('',#5971,#5972); -#5971 = CARTESIAN_POINT('',(69.722222222222,20.1,8.)); -#5972 = VECTOR('',#5973,1.); -#5973 = DIRECTION('',(0.,0.,1.)); -#5974 = ORIENTED_EDGE('',*,*,#5975,.F.); -#5975 = EDGE_CURVE('',#5952,#5968,#5976,.T.); -#5976 = LINE('',#5977,#5978); -#5977 = CARTESIAN_POINT('',(69.722222222222,17.9,40.)); -#5978 = VECTOR('',#5979,1.); -#5979 = DIRECTION('',(-0.,1.,0.)); -#5980 = PLANE('',#5981); -#5981 = AXIS2_PLACEMENT_3D('',#5982,#5983,#5984); -#5982 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#5983 = DIRECTION('',(1.,0.,-0.)); -#5984 = DIRECTION('',(0.,0.,1.)); -#5985 = ADVANCED_FACE('',(#5986),#6020,.T.); -#5986 = FACE_BOUND('',#5987,.T.); -#5987 = EDGE_LOOP('',(#5988,#5998,#6006,#6014)); -#5988 = ORIENTED_EDGE('',*,*,#5989,.F.); -#5989 = EDGE_CURVE('',#5990,#5992,#5994,.T.); -#5990 = VERTEX_POINT('',#5991); -#5991 = CARTESIAN_POINT('',(74.722222222222,17.9,8.)); -#5992 = VERTEX_POINT('',#5993); -#5993 = CARTESIAN_POINT('',(74.722222222222,17.9,40.)); -#5994 = LINE('',#5995,#5996); -#5995 = CARTESIAN_POINT('',(74.722222222222,17.9,8.)); -#5996 = VECTOR('',#5997,1.); -#5997 = DIRECTION('',(0.,0.,1.)); -#5998 = ORIENTED_EDGE('',*,*,#5999,.T.); -#5999 = EDGE_CURVE('',#5990,#6000,#6002,.T.); -#6000 = VERTEX_POINT('',#6001); -#6001 = CARTESIAN_POINT('',(74.722222222222,20.1,8.)); -#6002 = LINE('',#6003,#6004); -#6003 = CARTESIAN_POINT('',(74.722222222222,17.9,8.)); -#6004 = VECTOR('',#6005,1.); -#6005 = DIRECTION('',(-0.,1.,0.)); -#6006 = ORIENTED_EDGE('',*,*,#6007,.T.); -#6007 = EDGE_CURVE('',#6000,#6008,#6010,.T.); -#6008 = VERTEX_POINT('',#6009); -#6009 = CARTESIAN_POINT('',(74.722222222222,20.1,40.)); -#6010 = LINE('',#6011,#6012); -#6011 = CARTESIAN_POINT('',(74.722222222222,20.1,8.)); -#6012 = VECTOR('',#6013,1.); -#6013 = DIRECTION('',(0.,0.,1.)); -#6014 = ORIENTED_EDGE('',*,*,#6015,.F.); -#6015 = EDGE_CURVE('',#5992,#6008,#6016,.T.); -#6016 = LINE('',#6017,#6018); -#6017 = CARTESIAN_POINT('',(74.722222222222,17.9,40.)); -#6018 = VECTOR('',#6019,1.); -#6019 = DIRECTION('',(-0.,1.,0.)); -#6020 = PLANE('',#6021); -#6021 = AXIS2_PLACEMENT_3D('',#6022,#6023,#6024); -#6022 = CARTESIAN_POINT('',(74.722222222222,17.9,8.)); -#6023 = DIRECTION('',(1.,0.,-0.)); -#6024 = DIRECTION('',(0.,0.,1.)); -#6025 = ADVANCED_FACE('',(#6026),#6042,.F.); -#6026 = FACE_BOUND('',#6027,.F.); -#6027 = EDGE_LOOP('',(#6028,#6034,#6035,#6041)); -#6028 = ORIENTED_EDGE('',*,*,#6029,.F.); -#6029 = EDGE_CURVE('',#5950,#5990,#6030,.T.); -#6030 = LINE('',#6031,#6032); -#6031 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#6032 = VECTOR('',#6033,1.); -#6033 = DIRECTION('',(1.,0.,-0.)); -#6034 = ORIENTED_EDGE('',*,*,#5949,.T.); -#6035 = ORIENTED_EDGE('',*,*,#6036,.T.); -#6036 = EDGE_CURVE('',#5952,#5992,#6037,.T.); -#6037 = LINE('',#6038,#6039); -#6038 = CARTESIAN_POINT('',(69.722222222222,17.9,40.)); -#6039 = VECTOR('',#6040,1.); -#6040 = DIRECTION('',(1.,0.,-0.)); -#6041 = ORIENTED_EDGE('',*,*,#5989,.F.); -#6042 = PLANE('',#6043); -#6043 = AXIS2_PLACEMENT_3D('',#6044,#6045,#6046); -#6044 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#6045 = DIRECTION('',(-0.,1.,0.)); -#6046 = DIRECTION('',(0.,0.,1.)); -#6047 = ADVANCED_FACE('',(#6048),#6064,.T.); -#6048 = FACE_BOUND('',#6049,.T.); -#6049 = EDGE_LOOP('',(#6050,#6056,#6057,#6063)); -#6050 = ORIENTED_EDGE('',*,*,#6051,.F.); -#6051 = EDGE_CURVE('',#5960,#6000,#6052,.T.); -#6052 = LINE('',#6053,#6054); -#6053 = CARTESIAN_POINT('',(69.722222222222,20.1,8.)); -#6054 = VECTOR('',#6055,1.); -#6055 = DIRECTION('',(1.,0.,-0.)); -#6056 = ORIENTED_EDGE('',*,*,#5967,.T.); -#6057 = ORIENTED_EDGE('',*,*,#6058,.T.); -#6058 = EDGE_CURVE('',#5968,#6008,#6059,.T.); -#6059 = LINE('',#6060,#6061); -#6060 = CARTESIAN_POINT('',(69.722222222222,20.1,40.)); -#6061 = VECTOR('',#6062,1.); -#6062 = DIRECTION('',(1.,0.,-0.)); -#6063 = ORIENTED_EDGE('',*,*,#6007,.F.); -#6064 = PLANE('',#6065); -#6065 = AXIS2_PLACEMENT_3D('',#6066,#6067,#6068); -#6066 = CARTESIAN_POINT('',(69.722222222222,20.1,8.)); -#6067 = DIRECTION('',(-0.,1.,0.)); -#6068 = DIRECTION('',(0.,0.,1.)); -#6069 = ADVANCED_FACE('',(#6070),#6076,.F.); -#6070 = FACE_BOUND('',#6071,.F.); -#6071 = EDGE_LOOP('',(#6072,#6073,#6074,#6075)); -#6072 = ORIENTED_EDGE('',*,*,#5959,.F.); -#6073 = ORIENTED_EDGE('',*,*,#6029,.T.); -#6074 = ORIENTED_EDGE('',*,*,#5999,.T.); -#6075 = ORIENTED_EDGE('',*,*,#6051,.F.); -#6076 = PLANE('',#6077); -#6077 = AXIS2_PLACEMENT_3D('',#6078,#6079,#6080); -#6078 = CARTESIAN_POINT('',(69.722222222222,17.9,8.)); -#6079 = DIRECTION('',(0.,0.,1.)); -#6080 = DIRECTION('',(1.,0.,-0.)); -#6081 = ADVANCED_FACE('',(#6082),#6088,.T.); -#6082 = FACE_BOUND('',#6083,.T.); -#6083 = EDGE_LOOP('',(#6084,#6085,#6086,#6087)); -#6084 = ORIENTED_EDGE('',*,*,#5975,.F.); -#6085 = ORIENTED_EDGE('',*,*,#6036,.T.); -#6086 = ORIENTED_EDGE('',*,*,#6015,.T.); -#6087 = ORIENTED_EDGE('',*,*,#6058,.F.); -#6088 = PLANE('',#6089); -#6089 = AXIS2_PLACEMENT_3D('',#6090,#6091,#6092); -#6090 = CARTESIAN_POINT('',(69.722222222222,17.9,40.)); -#6091 = DIRECTION('',(0.,0.,1.)); -#6092 = DIRECTION('',(1.,0.,-0.)); -#6093 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6097)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#6094,#6095,#6096)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#6094 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#6095 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#6096 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#6097 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6094, - 'distance_accuracy_value','confusion accuracy'); -#6098 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6099,#6101); -#6099 = ( REPRESENTATION_RELATIONSHIP('','',#5942,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6100) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6100 = ITEM_DEFINED_TRANSFORMATION('','',#11,#151); -#6101 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6102); -#6102 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('35','WireDuct_RightCombSlot_16', - '',#5,#5937,$); -#6103 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#5939)); -#6104 = SHAPE_DEFINITION_REPRESENTATION(#6105,#6111); -#6105 = PRODUCT_DEFINITION_SHAPE('','',#6106); -#6106 = PRODUCT_DEFINITION('design','',#6107,#6110); -#6107 = PRODUCT_DEFINITION_FORMATION('','',#6108); -#6108 = PRODUCT('WireDuct_LeftCombSlot_17','WireDuct_LeftCombSlot_17','' - ,(#6109)); -#6109 = PRODUCT_CONTEXT('',#2,'mechanical'); -#6110 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#6111 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6112),#6262); -#6112 = MANIFOLD_SOLID_BREP('',#6113); -#6113 = CLOSED_SHELL('',(#6114,#6154,#6194,#6216,#6238,#6250)); -#6114 = ADVANCED_FACE('',(#6115),#6149,.F.); -#6115 = FACE_BOUND('',#6116,.F.); -#6116 = EDGE_LOOP('',(#6117,#6127,#6135,#6143)); -#6117 = ORIENTED_EDGE('',*,*,#6118,.F.); -#6118 = EDGE_CURVE('',#6119,#6121,#6123,.T.); -#6119 = VERTEX_POINT('',#6120); -#6120 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6121 = VERTEX_POINT('',#6122); -#6122 = CARTESIAN_POINT('',(80.833333333333,-20.1,40.)); -#6123 = LINE('',#6124,#6125); -#6124 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6125 = VECTOR('',#6126,1.); -#6126 = DIRECTION('',(0.,0.,1.)); -#6127 = ORIENTED_EDGE('',*,*,#6128,.T.); -#6128 = EDGE_CURVE('',#6119,#6129,#6131,.T.); -#6129 = VERTEX_POINT('',#6130); -#6130 = CARTESIAN_POINT('',(80.833333333333,-17.9,8.)); -#6131 = LINE('',#6132,#6133); -#6132 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6133 = VECTOR('',#6134,1.); -#6134 = DIRECTION('',(-0.,1.,0.)); -#6135 = ORIENTED_EDGE('',*,*,#6136,.T.); -#6136 = EDGE_CURVE('',#6129,#6137,#6139,.T.); -#6137 = VERTEX_POINT('',#6138); -#6138 = CARTESIAN_POINT('',(80.833333333333,-17.9,40.)); -#6139 = LINE('',#6140,#6141); -#6140 = CARTESIAN_POINT('',(80.833333333333,-17.9,8.)); -#6141 = VECTOR('',#6142,1.); -#6142 = DIRECTION('',(0.,0.,1.)); -#6143 = ORIENTED_EDGE('',*,*,#6144,.F.); -#6144 = EDGE_CURVE('',#6121,#6137,#6145,.T.); -#6145 = LINE('',#6146,#6147); -#6146 = CARTESIAN_POINT('',(80.833333333333,-20.1,40.)); -#6147 = VECTOR('',#6148,1.); -#6148 = DIRECTION('',(-0.,1.,0.)); -#6149 = PLANE('',#6150); -#6150 = AXIS2_PLACEMENT_3D('',#6151,#6152,#6153); -#6151 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6152 = DIRECTION('',(1.,0.,-0.)); -#6153 = DIRECTION('',(0.,0.,1.)); -#6154 = ADVANCED_FACE('',(#6155),#6189,.T.); -#6155 = FACE_BOUND('',#6156,.T.); -#6156 = EDGE_LOOP('',(#6157,#6167,#6175,#6183)); -#6157 = ORIENTED_EDGE('',*,*,#6158,.F.); -#6158 = EDGE_CURVE('',#6159,#6161,#6163,.T.); -#6159 = VERTEX_POINT('',#6160); -#6160 = CARTESIAN_POINT('',(85.833333333333,-20.1,8.)); -#6161 = VERTEX_POINT('',#6162); -#6162 = CARTESIAN_POINT('',(85.833333333333,-20.1,40.)); -#6163 = LINE('',#6164,#6165); -#6164 = CARTESIAN_POINT('',(85.833333333333,-20.1,8.)); -#6165 = VECTOR('',#6166,1.); -#6166 = DIRECTION('',(0.,0.,1.)); -#6167 = ORIENTED_EDGE('',*,*,#6168,.T.); -#6168 = EDGE_CURVE('',#6159,#6169,#6171,.T.); -#6169 = VERTEX_POINT('',#6170); -#6170 = CARTESIAN_POINT('',(85.833333333333,-17.9,8.)); -#6171 = LINE('',#6172,#6173); -#6172 = CARTESIAN_POINT('',(85.833333333333,-20.1,8.)); -#6173 = VECTOR('',#6174,1.); -#6174 = DIRECTION('',(-0.,1.,0.)); -#6175 = ORIENTED_EDGE('',*,*,#6176,.T.); -#6176 = EDGE_CURVE('',#6169,#6177,#6179,.T.); -#6177 = VERTEX_POINT('',#6178); -#6178 = CARTESIAN_POINT('',(85.833333333333,-17.9,40.)); -#6179 = LINE('',#6180,#6181); -#6180 = CARTESIAN_POINT('',(85.833333333333,-17.9,8.)); -#6181 = VECTOR('',#6182,1.); -#6182 = DIRECTION('',(0.,0.,1.)); -#6183 = ORIENTED_EDGE('',*,*,#6184,.F.); -#6184 = EDGE_CURVE('',#6161,#6177,#6185,.T.); -#6185 = LINE('',#6186,#6187); -#6186 = CARTESIAN_POINT('',(85.833333333333,-20.1,40.)); -#6187 = VECTOR('',#6188,1.); -#6188 = DIRECTION('',(-0.,1.,0.)); -#6189 = PLANE('',#6190); -#6190 = AXIS2_PLACEMENT_3D('',#6191,#6192,#6193); -#6191 = CARTESIAN_POINT('',(85.833333333333,-20.1,8.)); -#6192 = DIRECTION('',(1.,0.,-0.)); -#6193 = DIRECTION('',(0.,0.,1.)); -#6194 = ADVANCED_FACE('',(#6195),#6211,.F.); -#6195 = FACE_BOUND('',#6196,.F.); -#6196 = EDGE_LOOP('',(#6197,#6203,#6204,#6210)); -#6197 = ORIENTED_EDGE('',*,*,#6198,.F.); -#6198 = EDGE_CURVE('',#6119,#6159,#6199,.T.); -#6199 = LINE('',#6200,#6201); -#6200 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6201 = VECTOR('',#6202,1.); -#6202 = DIRECTION('',(1.,0.,-0.)); -#6203 = ORIENTED_EDGE('',*,*,#6118,.T.); -#6204 = ORIENTED_EDGE('',*,*,#6205,.T.); -#6205 = EDGE_CURVE('',#6121,#6161,#6206,.T.); -#6206 = LINE('',#6207,#6208); -#6207 = CARTESIAN_POINT('',(80.833333333333,-20.1,40.)); -#6208 = VECTOR('',#6209,1.); -#6209 = DIRECTION('',(1.,0.,-0.)); -#6210 = ORIENTED_EDGE('',*,*,#6158,.F.); -#6211 = PLANE('',#6212); -#6212 = AXIS2_PLACEMENT_3D('',#6213,#6214,#6215); -#6213 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6214 = DIRECTION('',(-0.,1.,0.)); -#6215 = DIRECTION('',(0.,0.,1.)); -#6216 = ADVANCED_FACE('',(#6217),#6233,.T.); -#6217 = FACE_BOUND('',#6218,.T.); -#6218 = EDGE_LOOP('',(#6219,#6225,#6226,#6232)); -#6219 = ORIENTED_EDGE('',*,*,#6220,.F.); -#6220 = EDGE_CURVE('',#6129,#6169,#6221,.T.); -#6221 = LINE('',#6222,#6223); -#6222 = CARTESIAN_POINT('',(80.833333333333,-17.9,8.)); -#6223 = VECTOR('',#6224,1.); -#6224 = DIRECTION('',(1.,0.,-0.)); -#6225 = ORIENTED_EDGE('',*,*,#6136,.T.); -#6226 = ORIENTED_EDGE('',*,*,#6227,.T.); -#6227 = EDGE_CURVE('',#6137,#6177,#6228,.T.); -#6228 = LINE('',#6229,#6230); -#6229 = CARTESIAN_POINT('',(80.833333333333,-17.9,40.)); -#6230 = VECTOR('',#6231,1.); -#6231 = DIRECTION('',(1.,0.,-0.)); -#6232 = ORIENTED_EDGE('',*,*,#6176,.F.); -#6233 = PLANE('',#6234); -#6234 = AXIS2_PLACEMENT_3D('',#6235,#6236,#6237); -#6235 = CARTESIAN_POINT('',(80.833333333333,-17.9,8.)); -#6236 = DIRECTION('',(-0.,1.,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('',*,*,#6198,.T.); -#6243 = ORIENTED_EDGE('',*,*,#6168,.T.); -#6244 = ORIENTED_EDGE('',*,*,#6220,.F.); -#6245 = PLANE('',#6246); -#6246 = AXIS2_PLACEMENT_3D('',#6247,#6248,#6249); -#6247 = CARTESIAN_POINT('',(80.833333333333,-20.1,8.)); -#6248 = DIRECTION('',(0.,0.,1.)); -#6249 = DIRECTION('',(1.,0.,-0.)); -#6250 = ADVANCED_FACE('',(#6251),#6257,.T.); -#6251 = FACE_BOUND('',#6252,.T.); -#6252 = EDGE_LOOP('',(#6253,#6254,#6255,#6256)); -#6253 = ORIENTED_EDGE('',*,*,#6144,.F.); -#6254 = ORIENTED_EDGE('',*,*,#6205,.T.); -#6255 = ORIENTED_EDGE('',*,*,#6184,.T.); -#6256 = ORIENTED_EDGE('',*,*,#6227,.F.); -#6257 = PLANE('',#6258); -#6258 = AXIS2_PLACEMENT_3D('',#6259,#6260,#6261); -#6259 = CARTESIAN_POINT('',(80.833333333333,-20.1,40.)); -#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('','',#6111,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6269) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6269 = ITEM_DEFINED_TRANSFORMATION('','',#11,#155); -#6270 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6271); -#6271 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('36','WireDuct_LeftCombSlot_17', - '',#5,#6106,$); -#6272 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6108)); -#6273 = SHAPE_DEFINITION_REPRESENTATION(#6274,#6280); -#6274 = PRODUCT_DEFINITION_SHAPE('','',#6275); -#6275 = PRODUCT_DEFINITION('design','',#6276,#6279); -#6276 = PRODUCT_DEFINITION_FORMATION('','',#6277); -#6277 = PRODUCT('WireDuct_RightCombSlot_17','WireDuct_RightCombSlot_17', - '',(#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('',(80.833333333333,17.9,8.)); -#6290 = VERTEX_POINT('',#6291); -#6291 = CARTESIAN_POINT('',(80.833333333333,17.9,40.)); -#6292 = LINE('',#6293,#6294); -#6293 = CARTESIAN_POINT('',(80.833333333333,17.9,8.)); -#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('',(80.833333333333,20.1,8.)); -#6300 = LINE('',#6301,#6302); -#6301 = CARTESIAN_POINT('',(80.833333333333,17.9,8.)); -#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('',(80.833333333333,20.1,40.)); -#6308 = LINE('',#6309,#6310); -#6309 = CARTESIAN_POINT('',(80.833333333333,20.1,8.)); -#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('',(80.833333333333,17.9,40.)); -#6316 = VECTOR('',#6317,1.); -#6317 = DIRECTION('',(-0.,1.,0.)); -#6318 = PLANE('',#6319); -#6319 = AXIS2_PLACEMENT_3D('',#6320,#6321,#6322); -#6320 = CARTESIAN_POINT('',(80.833333333333,17.9,8.)); -#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('',(85.833333333333,17.9,8.)); -#6330 = VERTEX_POINT('',#6331); -#6331 = CARTESIAN_POINT('',(85.833333333333,17.9,40.)); -#6332 = LINE('',#6333,#6334); -#6333 = CARTESIAN_POINT('',(85.833333333333,17.9,8.)); -#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('',(85.833333333333,20.1,8.)); -#6340 = LINE('',#6341,#6342); -#6341 = CARTESIAN_POINT('',(85.833333333333,17.9,8.)); -#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('',(85.833333333333,20.1,40.)); -#6348 = LINE('',#6349,#6350); -#6349 = CARTESIAN_POINT('',(85.833333333333,20.1,8.)); -#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('',(85.833333333333,17.9,40.)); -#6356 = VECTOR('',#6357,1.); -#6357 = DIRECTION('',(-0.,1.,0.)); -#6358 = PLANE('',#6359); -#6359 = AXIS2_PLACEMENT_3D('',#6360,#6361,#6362); -#6360 = CARTESIAN_POINT('',(85.833333333333,17.9,8.)); -#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('',(80.833333333333,17.9,8.)); -#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('',(80.833333333333,17.9,40.)); -#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('',(80.833333333333,17.9,8.)); -#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('',(80.833333333333,20.1,8.)); -#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('',(80.833333333333,20.1,40.)); -#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('',(80.833333333333,20.1,8.)); -#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('',(80.833333333333,17.9,8.)); -#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('',(80.833333333333,17.9,40.)); -#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,#159); -#6439 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6440); -#6440 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('37','WireDuct_RightCombSlot_17', - '',#5,#6275,$); -#6441 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6277)); -#6442 = SHAPE_DEFINITION_REPRESENTATION(#6443,#6449); -#6443 = PRODUCT_DEFINITION_SHAPE('','',#6444); -#6444 = PRODUCT_DEFINITION('design','',#6445,#6448); -#6445 = PRODUCT_DEFINITION_FORMATION('','',#6446); -#6446 = PRODUCT('WireDuct_LeftCombSlot_18','WireDuct_LeftCombSlot_18','' - ,(#6447)); -#6447 = PRODUCT_CONTEXT('',#2,'mechanical'); -#6448 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#6449 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6450),#6600); -#6450 = MANIFOLD_SOLID_BREP('',#6451); -#6451 = CLOSED_SHELL('',(#6452,#6492,#6532,#6554,#6576,#6588)); -#6452 = ADVANCED_FACE('',(#6453),#6487,.F.); -#6453 = FACE_BOUND('',#6454,.F.); -#6454 = EDGE_LOOP('',(#6455,#6465,#6473,#6481)); -#6455 = ORIENTED_EDGE('',*,*,#6456,.F.); -#6456 = EDGE_CURVE('',#6457,#6459,#6461,.T.); -#6457 = VERTEX_POINT('',#6458); -#6458 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6459 = VERTEX_POINT('',#6460); -#6460 = CARTESIAN_POINT('',(91.944444444444,-20.1,40.)); -#6461 = LINE('',#6462,#6463); -#6462 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6463 = VECTOR('',#6464,1.); -#6464 = DIRECTION('',(0.,0.,1.)); -#6465 = ORIENTED_EDGE('',*,*,#6466,.T.); -#6466 = EDGE_CURVE('',#6457,#6467,#6469,.T.); -#6467 = VERTEX_POINT('',#6468); -#6468 = CARTESIAN_POINT('',(91.944444444444,-17.9,8.)); -#6469 = LINE('',#6470,#6471); -#6470 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6471 = VECTOR('',#6472,1.); -#6472 = DIRECTION('',(-0.,1.,0.)); -#6473 = ORIENTED_EDGE('',*,*,#6474,.T.); -#6474 = EDGE_CURVE('',#6467,#6475,#6477,.T.); -#6475 = VERTEX_POINT('',#6476); -#6476 = CARTESIAN_POINT('',(91.944444444444,-17.9,40.)); -#6477 = LINE('',#6478,#6479); -#6478 = CARTESIAN_POINT('',(91.944444444444,-17.9,8.)); -#6479 = VECTOR('',#6480,1.); -#6480 = DIRECTION('',(0.,0.,1.)); -#6481 = ORIENTED_EDGE('',*,*,#6482,.F.); -#6482 = EDGE_CURVE('',#6459,#6475,#6483,.T.); -#6483 = LINE('',#6484,#6485); -#6484 = CARTESIAN_POINT('',(91.944444444444,-20.1,40.)); -#6485 = VECTOR('',#6486,1.); -#6486 = DIRECTION('',(-0.,1.,0.)); -#6487 = PLANE('',#6488); -#6488 = AXIS2_PLACEMENT_3D('',#6489,#6490,#6491); -#6489 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6490 = DIRECTION('',(1.,0.,-0.)); -#6491 = DIRECTION('',(0.,0.,1.)); -#6492 = ADVANCED_FACE('',(#6493),#6527,.T.); -#6493 = FACE_BOUND('',#6494,.T.); -#6494 = EDGE_LOOP('',(#6495,#6505,#6513,#6521)); -#6495 = ORIENTED_EDGE('',*,*,#6496,.F.); -#6496 = EDGE_CURVE('',#6497,#6499,#6501,.T.); -#6497 = VERTEX_POINT('',#6498); -#6498 = CARTESIAN_POINT('',(96.944444444444,-20.1,8.)); -#6499 = VERTEX_POINT('',#6500); -#6500 = CARTESIAN_POINT('',(96.944444444444,-20.1,40.)); -#6501 = LINE('',#6502,#6503); -#6502 = CARTESIAN_POINT('',(96.944444444444,-20.1,8.)); -#6503 = VECTOR('',#6504,1.); -#6504 = DIRECTION('',(0.,0.,1.)); -#6505 = ORIENTED_EDGE('',*,*,#6506,.T.); -#6506 = EDGE_CURVE('',#6497,#6507,#6509,.T.); -#6507 = VERTEX_POINT('',#6508); -#6508 = CARTESIAN_POINT('',(96.944444444444,-17.9,8.)); -#6509 = LINE('',#6510,#6511); -#6510 = CARTESIAN_POINT('',(96.944444444444,-20.1,8.)); -#6511 = VECTOR('',#6512,1.); -#6512 = DIRECTION('',(-0.,1.,0.)); -#6513 = ORIENTED_EDGE('',*,*,#6514,.T.); -#6514 = EDGE_CURVE('',#6507,#6515,#6517,.T.); -#6515 = VERTEX_POINT('',#6516); -#6516 = CARTESIAN_POINT('',(96.944444444444,-17.9,40.)); -#6517 = LINE('',#6518,#6519); -#6518 = CARTESIAN_POINT('',(96.944444444444,-17.9,8.)); -#6519 = VECTOR('',#6520,1.); -#6520 = DIRECTION('',(0.,0.,1.)); -#6521 = ORIENTED_EDGE('',*,*,#6522,.F.); -#6522 = EDGE_CURVE('',#6499,#6515,#6523,.T.); -#6523 = LINE('',#6524,#6525); -#6524 = CARTESIAN_POINT('',(96.944444444444,-20.1,40.)); -#6525 = VECTOR('',#6526,1.); -#6526 = DIRECTION('',(-0.,1.,0.)); -#6527 = PLANE('',#6528); -#6528 = AXIS2_PLACEMENT_3D('',#6529,#6530,#6531); -#6529 = CARTESIAN_POINT('',(96.944444444444,-20.1,8.)); -#6530 = DIRECTION('',(1.,0.,-0.)); -#6531 = DIRECTION('',(0.,0.,1.)); -#6532 = ADVANCED_FACE('',(#6533),#6549,.F.); -#6533 = FACE_BOUND('',#6534,.F.); -#6534 = EDGE_LOOP('',(#6535,#6541,#6542,#6548)); -#6535 = ORIENTED_EDGE('',*,*,#6536,.F.); -#6536 = EDGE_CURVE('',#6457,#6497,#6537,.T.); -#6537 = LINE('',#6538,#6539); -#6538 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6539 = VECTOR('',#6540,1.); -#6540 = DIRECTION('',(1.,0.,-0.)); -#6541 = ORIENTED_EDGE('',*,*,#6456,.T.); -#6542 = ORIENTED_EDGE('',*,*,#6543,.T.); -#6543 = EDGE_CURVE('',#6459,#6499,#6544,.T.); -#6544 = LINE('',#6545,#6546); -#6545 = CARTESIAN_POINT('',(91.944444444444,-20.1,40.)); -#6546 = VECTOR('',#6547,1.); -#6547 = DIRECTION('',(1.,0.,-0.)); -#6548 = ORIENTED_EDGE('',*,*,#6496,.F.); -#6549 = PLANE('',#6550); -#6550 = AXIS2_PLACEMENT_3D('',#6551,#6552,#6553); -#6551 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6552 = DIRECTION('',(-0.,1.,0.)); -#6553 = DIRECTION('',(0.,0.,1.)); -#6554 = ADVANCED_FACE('',(#6555),#6571,.T.); -#6555 = FACE_BOUND('',#6556,.T.); -#6556 = EDGE_LOOP('',(#6557,#6563,#6564,#6570)); -#6557 = ORIENTED_EDGE('',*,*,#6558,.F.); -#6558 = EDGE_CURVE('',#6467,#6507,#6559,.T.); -#6559 = LINE('',#6560,#6561); -#6560 = CARTESIAN_POINT('',(91.944444444444,-17.9,8.)); -#6561 = VECTOR('',#6562,1.); -#6562 = DIRECTION('',(1.,0.,-0.)); -#6563 = ORIENTED_EDGE('',*,*,#6474,.T.); -#6564 = ORIENTED_EDGE('',*,*,#6565,.T.); -#6565 = EDGE_CURVE('',#6475,#6515,#6566,.T.); -#6566 = LINE('',#6567,#6568); -#6567 = CARTESIAN_POINT('',(91.944444444444,-17.9,40.)); -#6568 = VECTOR('',#6569,1.); -#6569 = DIRECTION('',(1.,0.,-0.)); -#6570 = ORIENTED_EDGE('',*,*,#6514,.F.); -#6571 = PLANE('',#6572); -#6572 = AXIS2_PLACEMENT_3D('',#6573,#6574,#6575); -#6573 = CARTESIAN_POINT('',(91.944444444444,-17.9,8.)); -#6574 = DIRECTION('',(-0.,1.,0.)); -#6575 = DIRECTION('',(0.,0.,1.)); -#6576 = ADVANCED_FACE('',(#6577),#6583,.F.); -#6577 = FACE_BOUND('',#6578,.F.); -#6578 = EDGE_LOOP('',(#6579,#6580,#6581,#6582)); -#6579 = ORIENTED_EDGE('',*,*,#6466,.F.); -#6580 = ORIENTED_EDGE('',*,*,#6536,.T.); -#6581 = ORIENTED_EDGE('',*,*,#6506,.T.); -#6582 = ORIENTED_EDGE('',*,*,#6558,.F.); -#6583 = PLANE('',#6584); -#6584 = AXIS2_PLACEMENT_3D('',#6585,#6586,#6587); -#6585 = CARTESIAN_POINT('',(91.944444444444,-20.1,8.)); -#6586 = DIRECTION('',(0.,0.,1.)); -#6587 = DIRECTION('',(1.,0.,-0.)); -#6588 = ADVANCED_FACE('',(#6589),#6595,.T.); -#6589 = FACE_BOUND('',#6590,.T.); -#6590 = EDGE_LOOP('',(#6591,#6592,#6593,#6594)); -#6591 = ORIENTED_EDGE('',*,*,#6482,.F.); -#6592 = ORIENTED_EDGE('',*,*,#6543,.T.); -#6593 = ORIENTED_EDGE('',*,*,#6522,.T.); -#6594 = ORIENTED_EDGE('',*,*,#6565,.F.); -#6595 = PLANE('',#6596); -#6596 = AXIS2_PLACEMENT_3D('',#6597,#6598,#6599); -#6597 = CARTESIAN_POINT('',(91.944444444444,-20.1,40.)); -#6598 = DIRECTION('',(0.,0.,1.)); -#6599 = DIRECTION('',(1.,0.,-0.)); -#6600 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6604)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#6601,#6602,#6603)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#6601 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#6602 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#6603 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#6604 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6601, - 'distance_accuracy_value','confusion accuracy'); -#6605 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6606,#6608); -#6606 = ( REPRESENTATION_RELATIONSHIP('','',#6449,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6607) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6607 = ITEM_DEFINED_TRANSFORMATION('','',#11,#163); -#6608 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6609); -#6609 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('38','WireDuct_LeftCombSlot_18', - '',#5,#6444,$); -#6610 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6446)); -#6611 = SHAPE_DEFINITION_REPRESENTATION(#6612,#6618); -#6612 = PRODUCT_DEFINITION_SHAPE('','',#6613); -#6613 = PRODUCT_DEFINITION('design','',#6614,#6617); -#6614 = PRODUCT_DEFINITION_FORMATION('','',#6615); -#6615 = PRODUCT('WireDuct_RightCombSlot_18','WireDuct_RightCombSlot_18', - '',(#6616)); -#6616 = PRODUCT_CONTEXT('',#2,'mechanical'); -#6617 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#6618 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6619),#6769); -#6619 = MANIFOLD_SOLID_BREP('',#6620); -#6620 = CLOSED_SHELL('',(#6621,#6661,#6701,#6723,#6745,#6757)); -#6621 = ADVANCED_FACE('',(#6622),#6656,.F.); -#6622 = FACE_BOUND('',#6623,.F.); -#6623 = EDGE_LOOP('',(#6624,#6634,#6642,#6650)); -#6624 = ORIENTED_EDGE('',*,*,#6625,.F.); -#6625 = EDGE_CURVE('',#6626,#6628,#6630,.T.); -#6626 = VERTEX_POINT('',#6627); -#6627 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6628 = VERTEX_POINT('',#6629); -#6629 = CARTESIAN_POINT('',(91.944444444444,17.9,40.)); -#6630 = LINE('',#6631,#6632); -#6631 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6632 = VECTOR('',#6633,1.); -#6633 = DIRECTION('',(0.,0.,1.)); -#6634 = ORIENTED_EDGE('',*,*,#6635,.T.); -#6635 = EDGE_CURVE('',#6626,#6636,#6638,.T.); -#6636 = VERTEX_POINT('',#6637); -#6637 = CARTESIAN_POINT('',(91.944444444444,20.1,8.)); -#6638 = LINE('',#6639,#6640); -#6639 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6640 = VECTOR('',#6641,1.); -#6641 = DIRECTION('',(-0.,1.,0.)); -#6642 = ORIENTED_EDGE('',*,*,#6643,.T.); -#6643 = EDGE_CURVE('',#6636,#6644,#6646,.T.); -#6644 = VERTEX_POINT('',#6645); -#6645 = CARTESIAN_POINT('',(91.944444444444,20.1,40.)); -#6646 = LINE('',#6647,#6648); -#6647 = CARTESIAN_POINT('',(91.944444444444,20.1,8.)); -#6648 = VECTOR('',#6649,1.); -#6649 = DIRECTION('',(0.,0.,1.)); -#6650 = ORIENTED_EDGE('',*,*,#6651,.F.); -#6651 = EDGE_CURVE('',#6628,#6644,#6652,.T.); -#6652 = LINE('',#6653,#6654); -#6653 = CARTESIAN_POINT('',(91.944444444444,17.9,40.)); -#6654 = VECTOR('',#6655,1.); -#6655 = DIRECTION('',(-0.,1.,0.)); -#6656 = PLANE('',#6657); -#6657 = AXIS2_PLACEMENT_3D('',#6658,#6659,#6660); -#6658 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6659 = DIRECTION('',(1.,0.,-0.)); -#6660 = DIRECTION('',(0.,0.,1.)); -#6661 = ADVANCED_FACE('',(#6662),#6696,.T.); -#6662 = FACE_BOUND('',#6663,.T.); -#6663 = EDGE_LOOP('',(#6664,#6674,#6682,#6690)); -#6664 = ORIENTED_EDGE('',*,*,#6665,.F.); -#6665 = EDGE_CURVE('',#6666,#6668,#6670,.T.); -#6666 = VERTEX_POINT('',#6667); -#6667 = CARTESIAN_POINT('',(96.944444444444,17.9,8.)); -#6668 = VERTEX_POINT('',#6669); -#6669 = CARTESIAN_POINT('',(96.944444444444,17.9,40.)); -#6670 = LINE('',#6671,#6672); -#6671 = CARTESIAN_POINT('',(96.944444444444,17.9,8.)); -#6672 = VECTOR('',#6673,1.); -#6673 = DIRECTION('',(0.,0.,1.)); -#6674 = ORIENTED_EDGE('',*,*,#6675,.T.); -#6675 = EDGE_CURVE('',#6666,#6676,#6678,.T.); -#6676 = VERTEX_POINT('',#6677); -#6677 = CARTESIAN_POINT('',(96.944444444444,20.1,8.)); -#6678 = LINE('',#6679,#6680); -#6679 = CARTESIAN_POINT('',(96.944444444444,17.9,8.)); -#6680 = VECTOR('',#6681,1.); -#6681 = DIRECTION('',(-0.,1.,0.)); -#6682 = ORIENTED_EDGE('',*,*,#6683,.T.); -#6683 = EDGE_CURVE('',#6676,#6684,#6686,.T.); -#6684 = VERTEX_POINT('',#6685); -#6685 = CARTESIAN_POINT('',(96.944444444444,20.1,40.)); -#6686 = LINE('',#6687,#6688); -#6687 = CARTESIAN_POINT('',(96.944444444444,20.1,8.)); -#6688 = VECTOR('',#6689,1.); -#6689 = DIRECTION('',(0.,0.,1.)); -#6690 = ORIENTED_EDGE('',*,*,#6691,.F.); -#6691 = EDGE_CURVE('',#6668,#6684,#6692,.T.); -#6692 = LINE('',#6693,#6694); -#6693 = CARTESIAN_POINT('',(96.944444444444,17.9,40.)); -#6694 = VECTOR('',#6695,1.); -#6695 = DIRECTION('',(-0.,1.,0.)); -#6696 = PLANE('',#6697); -#6697 = AXIS2_PLACEMENT_3D('',#6698,#6699,#6700); -#6698 = CARTESIAN_POINT('',(96.944444444444,17.9,8.)); -#6699 = DIRECTION('',(1.,0.,-0.)); -#6700 = DIRECTION('',(0.,0.,1.)); -#6701 = ADVANCED_FACE('',(#6702),#6718,.F.); -#6702 = FACE_BOUND('',#6703,.F.); -#6703 = EDGE_LOOP('',(#6704,#6710,#6711,#6717)); -#6704 = ORIENTED_EDGE('',*,*,#6705,.F.); -#6705 = EDGE_CURVE('',#6626,#6666,#6706,.T.); -#6706 = LINE('',#6707,#6708); -#6707 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6708 = VECTOR('',#6709,1.); -#6709 = DIRECTION('',(1.,0.,-0.)); -#6710 = ORIENTED_EDGE('',*,*,#6625,.T.); -#6711 = ORIENTED_EDGE('',*,*,#6712,.T.); -#6712 = EDGE_CURVE('',#6628,#6668,#6713,.T.); -#6713 = LINE('',#6714,#6715); -#6714 = CARTESIAN_POINT('',(91.944444444444,17.9,40.)); -#6715 = VECTOR('',#6716,1.); -#6716 = DIRECTION('',(1.,0.,-0.)); -#6717 = ORIENTED_EDGE('',*,*,#6665,.F.); -#6718 = PLANE('',#6719); -#6719 = AXIS2_PLACEMENT_3D('',#6720,#6721,#6722); -#6720 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6721 = DIRECTION('',(-0.,1.,0.)); -#6722 = DIRECTION('',(0.,0.,1.)); -#6723 = ADVANCED_FACE('',(#6724),#6740,.T.); -#6724 = FACE_BOUND('',#6725,.T.); -#6725 = EDGE_LOOP('',(#6726,#6732,#6733,#6739)); -#6726 = ORIENTED_EDGE('',*,*,#6727,.F.); -#6727 = EDGE_CURVE('',#6636,#6676,#6728,.T.); -#6728 = LINE('',#6729,#6730); -#6729 = CARTESIAN_POINT('',(91.944444444444,20.1,8.)); -#6730 = VECTOR('',#6731,1.); -#6731 = DIRECTION('',(1.,0.,-0.)); -#6732 = ORIENTED_EDGE('',*,*,#6643,.T.); -#6733 = ORIENTED_EDGE('',*,*,#6734,.T.); -#6734 = EDGE_CURVE('',#6644,#6684,#6735,.T.); -#6735 = LINE('',#6736,#6737); -#6736 = CARTESIAN_POINT('',(91.944444444444,20.1,40.)); -#6737 = VECTOR('',#6738,1.); -#6738 = DIRECTION('',(1.,0.,-0.)); -#6739 = ORIENTED_EDGE('',*,*,#6683,.F.); -#6740 = PLANE('',#6741); -#6741 = AXIS2_PLACEMENT_3D('',#6742,#6743,#6744); -#6742 = CARTESIAN_POINT('',(91.944444444444,20.1,8.)); -#6743 = DIRECTION('',(-0.,1.,0.)); -#6744 = DIRECTION('',(0.,0.,1.)); -#6745 = ADVANCED_FACE('',(#6746),#6752,.F.); -#6746 = FACE_BOUND('',#6747,.F.); -#6747 = EDGE_LOOP('',(#6748,#6749,#6750,#6751)); -#6748 = ORIENTED_EDGE('',*,*,#6635,.F.); -#6749 = ORIENTED_EDGE('',*,*,#6705,.T.); -#6750 = ORIENTED_EDGE('',*,*,#6675,.T.); -#6751 = ORIENTED_EDGE('',*,*,#6727,.F.); -#6752 = PLANE('',#6753); -#6753 = AXIS2_PLACEMENT_3D('',#6754,#6755,#6756); -#6754 = CARTESIAN_POINT('',(91.944444444444,17.9,8.)); -#6755 = DIRECTION('',(0.,0.,1.)); -#6756 = DIRECTION('',(1.,0.,-0.)); -#6757 = ADVANCED_FACE('',(#6758),#6764,.T.); -#6758 = FACE_BOUND('',#6759,.T.); -#6759 = EDGE_LOOP('',(#6760,#6761,#6762,#6763)); -#6760 = ORIENTED_EDGE('',*,*,#6651,.F.); -#6761 = ORIENTED_EDGE('',*,*,#6712,.T.); -#6762 = ORIENTED_EDGE('',*,*,#6691,.T.); -#6763 = ORIENTED_EDGE('',*,*,#6734,.F.); -#6764 = PLANE('',#6765); -#6765 = AXIS2_PLACEMENT_3D('',#6766,#6767,#6768); -#6766 = CARTESIAN_POINT('',(91.944444444444,17.9,40.)); -#6767 = DIRECTION('',(0.,0.,1.)); -#6768 = DIRECTION('',(1.,0.,-0.)); -#6769 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6773)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#6770,#6771,#6772)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#6770 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#6771 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#6772 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#6773 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6770, - 'distance_accuracy_value','confusion accuracy'); -#6774 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6775,#6777); -#6775 = ( REPRESENTATION_RELATIONSHIP('','',#6618,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6776) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6776 = ITEM_DEFINED_TRANSFORMATION('','',#11,#167); -#6777 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6778); -#6778 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('39','WireDuct_RightCombSlot_18', - '',#5,#6613,$); -#6779 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6615)); -#6780 = SHAPE_DEFINITION_REPRESENTATION(#6781,#6787); -#6781 = PRODUCT_DEFINITION_SHAPE('','',#6782); -#6782 = PRODUCT_DEFINITION('design','',#6783,#6786); -#6783 = PRODUCT_DEFINITION_FORMATION('','',#6784); -#6784 = PRODUCT('WireDuct_MountHole__60','WireDuct_MountHole__60','',( - #6785)); -#6785 = PRODUCT_CONTEXT('',#2,'mechanical'); -#6786 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#6787 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6788),#6841); -#6788 = MANIFOLD_SOLID_BREP('',#6789); -#6789 = CLOSED_SHELL('',(#6790,#6823,#6832)); -#6790 = ADVANCED_FACE('',(#6791),#6818,.T.); -#6791 = FACE_BOUND('',#6792,.T.); -#6792 = EDGE_LOOP('',(#6793,#6802,#6810,#6817)); -#6793 = ORIENTED_EDGE('',*,*,#6794,.F.); -#6794 = EDGE_CURVE('',#6795,#6795,#6797,.T.); -#6795 = VERTEX_POINT('',#6796); -#6796 = CARTESIAN_POINT('',(-57.8,-5.388445916248E-16,2.2)); -#6797 = CIRCLE('',#6798,2.2); -#6798 = AXIS2_PLACEMENT_3D('',#6799,#6800,#6801); -#6799 = CARTESIAN_POINT('',(-60.,0.,2.2)); -#6800 = DIRECTION('',(0.,0.,1.)); -#6801 = DIRECTION('',(1.,0.,-0.)); -#6802 = ORIENTED_EDGE('',*,*,#6803,.F.); -#6803 = EDGE_CURVE('',#6804,#6795,#6806,.T.); -#6804 = VERTEX_POINT('',#6805); -#6805 = CARTESIAN_POINT('',(-57.8,-5.388445916248E-16,0.)); -#6806 = LINE('',#6807,#6808); -#6807 = CARTESIAN_POINT('',(-57.8,-5.388445916248E-16,0.)); -#6808 = VECTOR('',#6809,1.); -#6809 = DIRECTION('',(0.,0.,1.)); -#6810 = ORIENTED_EDGE('',*,*,#6811,.T.); -#6811 = EDGE_CURVE('',#6804,#6804,#6812,.T.); -#6812 = CIRCLE('',#6813,2.2); -#6813 = AXIS2_PLACEMENT_3D('',#6814,#6815,#6816); -#6814 = CARTESIAN_POINT('',(-60.,0.,0.)); -#6815 = DIRECTION('',(0.,0.,1.)); -#6816 = DIRECTION('',(1.,0.,-0.)); -#6817 = ORIENTED_EDGE('',*,*,#6803,.T.); -#6818 = CYLINDRICAL_SURFACE('',#6819,2.2); -#6819 = AXIS2_PLACEMENT_3D('',#6820,#6821,#6822); -#6820 = CARTESIAN_POINT('',(-60.,0.,0.)); -#6821 = DIRECTION('',(0.,0.,1.)); -#6822 = DIRECTION('',(1.,0.,-0.)); -#6823 = ADVANCED_FACE('',(#6824),#6827,.T.); -#6824 = FACE_BOUND('',#6825,.T.); -#6825 = EDGE_LOOP('',(#6826)); -#6826 = ORIENTED_EDGE('',*,*,#6794,.T.); -#6827 = PLANE('',#6828); -#6828 = AXIS2_PLACEMENT_3D('',#6829,#6830,#6831); -#6829 = CARTESIAN_POINT('',(-60.,0.,2.2)); -#6830 = DIRECTION('',(0.,0.,1.)); -#6831 = DIRECTION('',(1.,0.,-0.)); -#6832 = ADVANCED_FACE('',(#6833),#6836,.F.); -#6833 = FACE_BOUND('',#6834,.T.); -#6834 = EDGE_LOOP('',(#6835)); -#6835 = ORIENTED_EDGE('',*,*,#6811,.F.); -#6836 = PLANE('',#6837); -#6837 = AXIS2_PLACEMENT_3D('',#6838,#6839,#6840); -#6838 = CARTESIAN_POINT('',(-60.,0.,0.)); -#6839 = DIRECTION('',(0.,0.,1.)); -#6840 = DIRECTION('',(1.,0.,-0.)); -#6841 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6845)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#6842,#6843,#6844)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#6842 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#6843 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#6844 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#6845 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6842, - 'distance_accuracy_value','confusion accuracy'); -#6846 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6847,#6849); -#6847 = ( REPRESENTATION_RELATIONSHIP('','',#6787,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6848) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6848 = ITEM_DEFINED_TRANSFORMATION('','',#11,#171); -#6849 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6850); -#6850 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('40','WireDuct_MountHole__60','', - #5,#6782,$); -#6851 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6784)); -#6852 = SHAPE_DEFINITION_REPRESENTATION(#6853,#6859); -#6853 = PRODUCT_DEFINITION_SHAPE('','',#6854); -#6854 = PRODUCT_DEFINITION('design','',#6855,#6858); -#6855 = PRODUCT_DEFINITION_FORMATION('','',#6856); -#6856 = PRODUCT('WireDuct_MountHole_0','WireDuct_MountHole_0','',(#6857) - ); -#6857 = PRODUCT_CONTEXT('',#2,'mechanical'); -#6858 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#6859 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6860),#6913); -#6860 = MANIFOLD_SOLID_BREP('',#6861); -#6861 = CLOSED_SHELL('',(#6862,#6895,#6904)); -#6862 = ADVANCED_FACE('',(#6863),#6890,.T.); -#6863 = FACE_BOUND('',#6864,.T.); -#6864 = EDGE_LOOP('',(#6865,#6874,#6882,#6889)); -#6865 = ORIENTED_EDGE('',*,*,#6866,.F.); -#6866 = EDGE_CURVE('',#6867,#6867,#6869,.T.); -#6867 = VERTEX_POINT('',#6868); -#6868 = CARTESIAN_POINT('',(2.2,-5.388445916248E-16,2.2)); -#6869 = CIRCLE('',#6870,2.2); -#6870 = AXIS2_PLACEMENT_3D('',#6871,#6872,#6873); -#6871 = CARTESIAN_POINT('',(0.,0.,2.2)); -#6872 = DIRECTION('',(0.,0.,1.)); -#6873 = DIRECTION('',(1.,0.,-0.)); -#6874 = ORIENTED_EDGE('',*,*,#6875,.F.); -#6875 = EDGE_CURVE('',#6876,#6867,#6878,.T.); -#6876 = VERTEX_POINT('',#6877); -#6877 = CARTESIAN_POINT('',(2.2,-5.388445916248E-16,0.)); -#6878 = LINE('',#6879,#6880); -#6879 = CARTESIAN_POINT('',(2.2,-5.388445916248E-16,0.)); -#6880 = VECTOR('',#6881,1.); -#6881 = DIRECTION('',(0.,0.,1.)); -#6882 = ORIENTED_EDGE('',*,*,#6883,.T.); -#6883 = EDGE_CURVE('',#6876,#6876,#6884,.T.); -#6884 = CIRCLE('',#6885,2.2); -#6885 = AXIS2_PLACEMENT_3D('',#6886,#6887,#6888); -#6886 = CARTESIAN_POINT('',(0.,0.,0.)); -#6887 = DIRECTION('',(0.,0.,1.)); -#6888 = DIRECTION('',(1.,0.,-0.)); -#6889 = ORIENTED_EDGE('',*,*,#6875,.T.); -#6890 = CYLINDRICAL_SURFACE('',#6891,2.2); -#6891 = AXIS2_PLACEMENT_3D('',#6892,#6893,#6894); -#6892 = CARTESIAN_POINT('',(0.,0.,0.)); -#6893 = DIRECTION('',(0.,0.,1.)); -#6894 = DIRECTION('',(1.,0.,-0.)); -#6895 = ADVANCED_FACE('',(#6896),#6899,.T.); -#6896 = FACE_BOUND('',#6897,.T.); -#6897 = EDGE_LOOP('',(#6898)); -#6898 = ORIENTED_EDGE('',*,*,#6866,.T.); -#6899 = PLANE('',#6900); -#6900 = AXIS2_PLACEMENT_3D('',#6901,#6902,#6903); -#6901 = CARTESIAN_POINT('',(0.,0.,2.2)); -#6902 = DIRECTION('',(0.,0.,1.)); -#6903 = DIRECTION('',(1.,0.,-0.)); -#6904 = ADVANCED_FACE('',(#6905),#6908,.F.); -#6905 = FACE_BOUND('',#6906,.T.); -#6906 = EDGE_LOOP('',(#6907)); -#6907 = ORIENTED_EDGE('',*,*,#6883,.F.); -#6908 = PLANE('',#6909); -#6909 = AXIS2_PLACEMENT_3D('',#6910,#6911,#6912); -#6910 = CARTESIAN_POINT('',(0.,0.,0.)); -#6911 = DIRECTION('',(0.,0.,1.)); -#6912 = DIRECTION('',(1.,0.,-0.)); -#6913 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6917)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#6914,#6915,#6916)) REPRESENTATION_CONTEXT -('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#6914 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#6915 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#6916 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#6917 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6914, - 'distance_accuracy_value','confusion accuracy'); -#6918 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6919,#6921); -#6919 = ( REPRESENTATION_RELATIONSHIP('','',#6859,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6920) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6920 = ITEM_DEFINED_TRANSFORMATION('','',#11,#175); -#6921 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6922); -#6922 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('41','WireDuct_MountHole_0','',#5 - ,#6854,$); -#6923 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6856)); -#6924 = SHAPE_DEFINITION_REPRESENTATION(#6925,#6931); -#6925 = PRODUCT_DEFINITION_SHAPE('','',#6926); -#6926 = PRODUCT_DEFINITION('design','',#6927,#6930); -#6927 = PRODUCT_DEFINITION_FORMATION('','',#6928); -#6928 = PRODUCT('WireDuct_MountHole_60','WireDuct_MountHole_60','',( - #6929)); -#6929 = PRODUCT_CONTEXT('',#2,'mechanical'); -#6930 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); -#6931 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6932),#6985); -#6932 = MANIFOLD_SOLID_BREP('',#6933); -#6933 = CLOSED_SHELL('',(#6934,#6967,#6976)); -#6934 = ADVANCED_FACE('',(#6935),#6962,.T.); -#6935 = FACE_BOUND('',#6936,.T.); -#6936 = EDGE_LOOP('',(#6937,#6946,#6954,#6961)); -#6937 = ORIENTED_EDGE('',*,*,#6938,.F.); -#6938 = EDGE_CURVE('',#6939,#6939,#6941,.T.); -#6939 = VERTEX_POINT('',#6940); -#6940 = CARTESIAN_POINT('',(62.2,-5.388445916248E-16,2.2)); -#6941 = CIRCLE('',#6942,2.2); -#6942 = AXIS2_PLACEMENT_3D('',#6943,#6944,#6945); -#6943 = CARTESIAN_POINT('',(60.,0.,2.2)); -#6944 = DIRECTION('',(0.,0.,1.)); -#6945 = DIRECTION('',(1.,0.,-0.)); -#6946 = ORIENTED_EDGE('',*,*,#6947,.F.); -#6947 = EDGE_CURVE('',#6948,#6939,#6950,.T.); -#6948 = VERTEX_POINT('',#6949); -#6949 = CARTESIAN_POINT('',(62.2,-5.388445916248E-16,0.)); -#6950 = LINE('',#6951,#6952); -#6951 = CARTESIAN_POINT('',(62.2,-5.388445916248E-16,0.)); -#6952 = VECTOR('',#6953,1.); -#6953 = DIRECTION('',(0.,0.,1.)); -#6954 = ORIENTED_EDGE('',*,*,#6955,.T.); -#6955 = EDGE_CURVE('',#6948,#6948,#6956,.T.); -#6956 = CIRCLE('',#6957,2.2); -#6957 = AXIS2_PLACEMENT_3D('',#6958,#6959,#6960); -#6958 = CARTESIAN_POINT('',(60.,0.,0.)); -#6959 = DIRECTION('',(0.,0.,1.)); -#6960 = DIRECTION('',(1.,0.,-0.)); -#6961 = ORIENTED_EDGE('',*,*,#6947,.T.); -#6962 = CYLINDRICAL_SURFACE('',#6963,2.2); -#6963 = AXIS2_PLACEMENT_3D('',#6964,#6965,#6966); -#6964 = CARTESIAN_POINT('',(60.,0.,0.)); -#6965 = DIRECTION('',(0.,0.,1.)); -#6966 = DIRECTION('',(1.,0.,-0.)); -#6967 = ADVANCED_FACE('',(#6968),#6971,.T.); -#6968 = FACE_BOUND('',#6969,.T.); -#6969 = EDGE_LOOP('',(#6970)); -#6970 = ORIENTED_EDGE('',*,*,#6938,.T.); -#6971 = PLANE('',#6972); -#6972 = AXIS2_PLACEMENT_3D('',#6973,#6974,#6975); -#6973 = CARTESIAN_POINT('',(60.,0.,2.2)); -#6974 = DIRECTION('',(0.,0.,1.)); -#6975 = DIRECTION('',(1.,0.,-0.)); -#6976 = ADVANCED_FACE('',(#6977),#6980,.F.); -#6977 = FACE_BOUND('',#6978,.T.); -#6978 = EDGE_LOOP('',(#6979)); -#6979 = ORIENTED_EDGE('',*,*,#6955,.F.); -#6980 = PLANE('',#6981); -#6981 = AXIS2_PLACEMENT_3D('',#6982,#6983,#6984); -#6982 = CARTESIAN_POINT('',(60.,0.,0.)); -#6983 = DIRECTION('',(0.,0.,1.)); -#6984 = DIRECTION('',(1.,0.,-0.)); -#6985 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) -GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6989)) -GLOBAL_UNIT_ASSIGNED_CONTEXT((#6986,#6987,#6988)) REPRESENTATION_CONTEXT +#5304 = ORIENTED_EDGE('',*,*,#2847,.T.); +#5305 = ORIENTED_EDGE('',*,*,#5273,.T.); +#5306 = ORIENTED_EDGE('',*,*,#5307,.F.); +#5307 = EDGE_CURVE('',#5298,#5274,#5308,.T.); +#5308 = LINE('',#5309,#5310); +#5309 = CARTESIAN_POINT('',(-96.94444444444,20.,4.)); +#5310 = VECTOR('',#5311,1.); +#5311 = DIRECTION('',(-0.,0.,-1.)); +#5312 = PLANE('',#5313); +#5313 = AXIS2_PLACEMENT_3D('',#5314,#5315,#5316); +#5314 = CARTESIAN_POINT('',(-96.94444444444,17.8,8.)); +#5315 = DIRECTION('',(1.,0.,-0.)); +#5316 = DIRECTION('',(0.,0.,1.)); +#5317 = ADVANCED_FACE('',(#5318),#5329,.T.); +#5318 = FACE_BOUND('',#5319,.T.); +#5319 = EDGE_LOOP('',(#5320,#5326,#5327,#5328)); +#5320 = ORIENTED_EDGE('',*,*,#5321,.F.); +#5321 = EDGE_CURVE('',#3483,#5298,#5322,.T.); +#5322 = LINE('',#5323,#5324); +#5323 = CARTESIAN_POINT('',(-100.,20.,40.)); +#5324 = VECTOR('',#5325,1.); +#5325 = DIRECTION('',(1.,0.,-0.)); +#5326 = ORIENTED_EDGE('',*,*,#3490,.F.); +#5327 = ORIENTED_EDGE('',*,*,#2839,.T.); +#5328 = ORIENTED_EDGE('',*,*,#5297,.T.); +#5329 = PLANE('',#5330); +#5330 = AXIS2_PLACEMENT_3D('',#5331,#5332,#5333); +#5331 = CARTESIAN_POINT('',(-100.,18.,40.)); +#5332 = DIRECTION('',(0.,0.,1.)); +#5333 = DIRECTION('',(1.,0.,-0.)); +#5334 = ADVANCED_FACE('',(#5335),#5413,.T.); +#5335 = FACE_BOUND('',#5336,.T.); +#5336 = EDGE_LOOP('',(#5337,#5338,#5339,#5340,#5341,#5342,#5343,#5344, + #5345,#5346,#5347,#5348,#5349,#5350,#5351,#5352,#5353,#5354,#5355, + #5356,#5357,#5358,#5359,#5360,#5361,#5362,#5363,#5364,#5365,#5366, + #5367,#5368,#5369,#5370,#5371,#5372,#5373,#5374,#5375,#5376,#5377, + #5378,#5379,#5380,#5381,#5382,#5383,#5384,#5385,#5386,#5387,#5388, + #5389,#5390,#5391,#5392,#5393,#5394,#5395,#5396,#5397,#5398,#5399, + #5400,#5401,#5402,#5403,#5404,#5405,#5406,#5407,#5408,#5409,#5410, + #5411,#5412)); +#5337 = ORIENTED_EDGE('',*,*,#3513,.F.); +#5338 = ORIENTED_EDGE('',*,*,#3482,.T.); +#5339 = ORIENTED_EDGE('',*,*,#5321,.T.); +#5340 = ORIENTED_EDGE('',*,*,#5307,.T.); +#5341 = ORIENTED_EDGE('',*,*,#5283,.T.); +#5342 = ORIENTED_EDGE('',*,*,#5259,.F.); +#5343 = ORIENTED_EDGE('',*,*,#5225,.T.); +#5344 = ORIENTED_EDGE('',*,*,#5211,.T.); +#5345 = ORIENTED_EDGE('',*,*,#5187,.T.); +#5346 = ORIENTED_EDGE('',*,*,#5163,.F.); +#5347 = ORIENTED_EDGE('',*,*,#5129,.T.); +#5348 = ORIENTED_EDGE('',*,*,#5115,.T.); +#5349 = ORIENTED_EDGE('',*,*,#5091,.T.); +#5350 = ORIENTED_EDGE('',*,*,#5067,.F.); +#5351 = ORIENTED_EDGE('',*,*,#5033,.T.); +#5352 = ORIENTED_EDGE('',*,*,#5019,.T.); +#5353 = ORIENTED_EDGE('',*,*,#4995,.T.); +#5354 = ORIENTED_EDGE('',*,*,#4971,.F.); +#5355 = ORIENTED_EDGE('',*,*,#4937,.T.); +#5356 = ORIENTED_EDGE('',*,*,#4923,.T.); +#5357 = ORIENTED_EDGE('',*,*,#4899,.T.); +#5358 = ORIENTED_EDGE('',*,*,#4875,.F.); +#5359 = ORIENTED_EDGE('',*,*,#4841,.T.); +#5360 = ORIENTED_EDGE('',*,*,#4827,.T.); +#5361 = ORIENTED_EDGE('',*,*,#4803,.T.); +#5362 = ORIENTED_EDGE('',*,*,#4779,.F.); +#5363 = ORIENTED_EDGE('',*,*,#4745,.T.); +#5364 = ORIENTED_EDGE('',*,*,#4731,.T.); +#5365 = ORIENTED_EDGE('',*,*,#4707,.T.); +#5366 = ORIENTED_EDGE('',*,*,#4683,.F.); +#5367 = ORIENTED_EDGE('',*,*,#4649,.T.); +#5368 = ORIENTED_EDGE('',*,*,#4635,.T.); +#5369 = ORIENTED_EDGE('',*,*,#4611,.T.); +#5370 = ORIENTED_EDGE('',*,*,#4587,.F.); +#5371 = ORIENTED_EDGE('',*,*,#4553,.T.); +#5372 = ORIENTED_EDGE('',*,*,#4539,.T.); +#5373 = ORIENTED_EDGE('',*,*,#4515,.T.); +#5374 = ORIENTED_EDGE('',*,*,#4491,.F.); +#5375 = ORIENTED_EDGE('',*,*,#4457,.T.); +#5376 = ORIENTED_EDGE('',*,*,#4443,.T.); +#5377 = ORIENTED_EDGE('',*,*,#4419,.T.); +#5378 = ORIENTED_EDGE('',*,*,#4395,.F.); +#5379 = ORIENTED_EDGE('',*,*,#4361,.T.); +#5380 = ORIENTED_EDGE('',*,*,#4347,.T.); +#5381 = ORIENTED_EDGE('',*,*,#4323,.T.); +#5382 = ORIENTED_EDGE('',*,*,#4299,.F.); +#5383 = ORIENTED_EDGE('',*,*,#4265,.T.); +#5384 = ORIENTED_EDGE('',*,*,#4251,.T.); +#5385 = ORIENTED_EDGE('',*,*,#4227,.T.); +#5386 = ORIENTED_EDGE('',*,*,#4203,.F.); +#5387 = ORIENTED_EDGE('',*,*,#4169,.T.); +#5388 = ORIENTED_EDGE('',*,*,#4155,.T.); +#5389 = ORIENTED_EDGE('',*,*,#4131,.T.); +#5390 = ORIENTED_EDGE('',*,*,#4107,.F.); +#5391 = ORIENTED_EDGE('',*,*,#4073,.T.); +#5392 = ORIENTED_EDGE('',*,*,#4059,.T.); +#5393 = ORIENTED_EDGE('',*,*,#4035,.T.); +#5394 = ORIENTED_EDGE('',*,*,#4011,.F.); +#5395 = ORIENTED_EDGE('',*,*,#3977,.T.); +#5396 = ORIENTED_EDGE('',*,*,#3963,.T.); +#5397 = ORIENTED_EDGE('',*,*,#3939,.T.); +#5398 = ORIENTED_EDGE('',*,*,#3915,.F.); +#5399 = ORIENTED_EDGE('',*,*,#3881,.T.); +#5400 = ORIENTED_EDGE('',*,*,#3867,.T.); +#5401 = ORIENTED_EDGE('',*,*,#3843,.T.); +#5402 = ORIENTED_EDGE('',*,*,#3819,.F.); +#5403 = ORIENTED_EDGE('',*,*,#3785,.T.); +#5404 = ORIENTED_EDGE('',*,*,#3771,.T.); +#5405 = ORIENTED_EDGE('',*,*,#3747,.T.); +#5406 = ORIENTED_EDGE('',*,*,#3723,.F.); +#5407 = ORIENTED_EDGE('',*,*,#3689,.T.); +#5408 = ORIENTED_EDGE('',*,*,#3675,.T.); +#5409 = ORIENTED_EDGE('',*,*,#3651,.T.); +#5410 = ORIENTED_EDGE('',*,*,#3627,.F.); +#5411 = ORIENTED_EDGE('',*,*,#3593,.T.); +#5412 = ORIENTED_EDGE('',*,*,#3571,.F.); +#5413 = PLANE('',#5414); +#5414 = AXIS2_PLACEMENT_3D('',#5415,#5416,#5417); +#5415 = CARTESIAN_POINT('',(-100.,20.,0.)); +#5416 = DIRECTION('',(-0.,1.,0.)); +#5417 = DIRECTION('',(0.,0.,1.)); +#5418 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#5422)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#5419,#5420,#5421)) REPRESENTATION_CONTEXT ('Context #1','3D Context with UNIT and UNCERTAINTY') ); -#6986 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); -#6987 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); -#6988 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); -#6989 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#6986, +#5419 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#5420 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#5421 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#5422 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-07),#5419, 'distance_accuracy_value','confusion accuracy'); -#6990 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6991,#6993); -#6991 = ( REPRESENTATION_RELATIONSHIP('','',#6931,#10) -REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6992) -SHAPE_REPRESENTATION_RELATIONSHIP() ); -#6992 = ITEM_DEFINED_TRANSFORMATION('','',#11,#179); -#6993 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', - #6994); -#6994 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('42','WireDuct_MountHole_60','', - #5,#6926,$); -#6995 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6928)); +#5423 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); ENDSEC; END-ISO-10303-21; From 4b40e2f81d6135bd78336a860d313c937d61f3c9 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 14:43:55 +0800 Subject: [PATCH 23/63] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=B8=83=E7=BA=BF=E9=94=99=E5=BC=80=E6=96=B9=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 27 ++++++++-- .../freecad_exchange_auto_routing_test.py | 49 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 83af255..d87e7ce 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -23,7 +23,7 @@ import WiringObjects DEFAULT_OPTIONS = { # 端子出来先走一小段,避免导线贴着设备外壳起步。 "terminal_exit_length": 20.0, - "lane_axis": "y", + "lane_axis": "auto", "lane_spacing": 10.0, # 线槽网络相关参数。 "use_routing_network": True, @@ -139,9 +139,30 @@ def _with_axis(point, axis, value): ) -def _lane_payload(route_index, options): +def _auto_lane_axis(route_points): + points = [_vector(point) for point in route_points or []] + if len(points) < 2: + return "y" + 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 dominant_axis == "y": + return "x" + if dominant_axis == "x": + return "y" + return "x" + + +def _lane_payload(route_index, options, route_points=None): opts = options or {} lane_axis = (opts.get("lane_axis") or "y").lower() + if lane_axis == "auto": + lane_axis = _auto_lane_axis(route_points) if lane_axis not in {"x", "y", "z"}: lane_axis = "y" lane_index = max(int(route_index or 0), 0) @@ -778,7 +799,7 @@ 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) + lane = _lane_payload(route_index, opts, route_points=carrier_points) carrier_points = _apply_lane_offset(carrier_points, lane) points = [] diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 43d8aaf..0c86ca8 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1918,6 +1918,55 @@ class AutoRoutingTest(unittest.TestCase): ][0] self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + def test_route_eplan_connections_auto_lane_axis_offsets_perpendicular_to_shared_segment(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0}, + ) + + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + self.assertEqual("x", report["routes"][1]["lane"]["axis"]) + routed_group = doc.getObject("QETWiring_04_Routed") + second_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "wire-b" + ][0] + self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From be007035bec98feb072922a79b48decbcec1efd8 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 14:53:19 +0800 Subject: [PATCH 24/63] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E4=BC=98=E5=85=88=E4=BD=BF=E7=94=A8=E7=A9=BA=E9=97=B2?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 14 +++- src/Mod/FreeCADExchange/RoutingNetwork.py | 29 +++++++- .../freecad_exchange_auto_routing_test.py | 68 +++++++++++++++++++ 3 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index d87e7ce..46622ec 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -25,6 +25,7 @@ DEFAULT_OPTIONS = { "terminal_exit_length": 20.0, "lane_axis": "auto", "lane_spacing": 10.0, + "segment_reuse_penalty": 200.0, # 线槽网络相关参数。 "use_routing_network": True, "network_entry_max_distance": 1000.0, @@ -791,6 +792,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non end_key, bend_penalty=float(opts.get("bend_penalty", 0.0) or 0.0), kind_cost_factors=opts.get("carrier_kind_cost_factors", {}), + segment_usage_costs=opts.get("segment_usage_costs", {}), + segment_reuse_penalty=float(opts.get("segment_reuse_penalty", 0.0) or 0.0), ) path_keys = path_result.get("path", []) if isinstance(path_result, dict) else [] if not path_keys: @@ -1424,18 +1427,22 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la missing_endpoint_uuids = set() lane_indexes_by_pair = {} lane_indexes_by_segment = {} + segment_usage_costs = {} def add_status(status): key = str(status or "").strip() or "Unknown" report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): + route_options = dict(options or {}) + if isinstance(item, dict) and "__segment_usage_costs" in item: + route_options["segment_usage_costs"] = item.get("__segment_usage_costs", {}) return route_eplan_connection_between_terminals( doc, start_terminal, end_terminal, route_index=route_lane_index, - options=options, + options=route_options, wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), wire_label=_wire_item_value(item, "wire_label", "wire_mark"), net_uuid=_wire_item_value(item, "net_uuid"), @@ -1492,7 +1499,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } result = create_route( route_lane_index, - item, + dict(item, __segment_usage_costs=segment_usage_costs), start_terminal, end_terminal, endpoint_metadata, @@ -1507,7 +1514,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la try: result = create_route( final_lane_index, - item, + dict(item, __segment_usage_costs=segment_usage_costs), start_terminal, end_terminal, endpoint_metadata, @@ -1548,6 +1555,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la lane_indexes_by_segment.get(segment_key, 0), int(result.get("lane", {}).get("index", 0) or 0) + 1, ) + segment_usage_costs[segment_key] = segment_usage_costs.get(segment_key, 0) + 1 if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 add_status(result["route_status"]) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index fb38574..5ac5f4a 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2270,7 +2270,23 @@ def _carrier_track_payload(carrier): } -def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, kind_cost_factors=None): +def _segment_usage_key(carrier, from_key, to_key): + carrier_name = getattr(carrier, "Name", "") if carrier is not None else "" + return ( + carrier_name, + tuple(sorted((from_key, to_key))), + ) + + +def shortest_path_with_carriers( + network, + start_key, + end_key, + bend_penalty=0.0, + kind_cost_factors=None, + segment_usage_costs=None, + segment_reuse_penalty=0.0, +): """Dijkstra search with a small extra cost when route direction changes.""" if start_key is None or end_key is None: return None @@ -2343,8 +2359,17 @@ def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, k bend_cost = 0.0 if previous_direction is not None and direction != previous_direction: bend_cost = float(bend_penalty or 0.0) + usage_cost = 0.0 + if segment_usage_costs: + usage_count = float(segment_usage_costs.get(_segment_usage_key(carrier, key, next_key), 0.0) or 0.0) + usage_cost = usage_count * float(segment_reuse_penalty or 0.0) next_state = (next_key, direction) - next_cost = cost + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) + bend_cost + next_cost = ( + cost + + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) + + bend_cost + + usage_cost + ) if next_cost < distances.get(next_state, float("inf")): distances[next_state] = next_cost previous[next_state] = { diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 0c86ca8..3787c85 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1967,6 +1967,74 @@ 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_prefers_unused_alternate_route_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, "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( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + first_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + second_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][1]["route_track"]["segments"] + ] + self.assertIn("Direct Duct", first_labels) + self.assertIn("Alternate Duct", second_labels) + self.assertNotIn("Direct Duct", second_labels) + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 0fe5144257d9d974eeae5d3018bcd4e9c06e7592 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 15:00:36 +0800 Subject: [PATCH 25/63] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E9=81=BF=E5=BC=80=E5=B7=B2=E6=9C=89=E5=AF=BC=E7=BA=BF?= =?UTF-8?q?=E5=8D=A0=E7=94=A8=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 38 +++++++++++- .../freecad_exchange_auto_routing_test.py | 62 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 46622ec..08ec063 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1337,6 +1337,10 @@ def _route_segment_key(segment): def _route_segment_keys(result): route_track = result.get("route_track", {}) if isinstance(result, dict) else {} + return _route_track_segment_keys(route_track) + + +def _route_track_segment_keys(route_track): segments = route_track.get("segments", []) if isinstance(route_track, dict) else [] keys = [] for segment in segments or []: @@ -1346,6 +1350,35 @@ def _route_segment_keys(result): return keys +def _incoming_wire_uuids(wires): + wire_uuids = set() + for item in wires or []: + if not isinstance(item, dict): + continue + wire_uuid = _wire_item_value(item, "wire_id", "wire_uuid", "id") + if wire_uuid: + wire_uuids.add(wire_uuid) + return wire_uuids + + +def _existing_routed_segment_usage(doc, excluded_wire_uuids=None): + excluded_wire_uuids = set(excluded_wire_uuids or []) + usage = {} + for wire in list(WiringObjects.iter_routed_wire_objects(doc)): + if (getattr(wire, "RouteType", "") or "").strip() != "RoutedConnection": + continue + wire_uuid = (getattr(wire, "QetWireUuid", "") or "").strip() + if wire_uuid and wire_uuid in excluded_wire_uuids: + continue + try: + route_track = json.loads((getattr(wire, "QetRouteTrackJson", "") or "").strip() or "{}") + except Exception: + route_track = {} + for segment_key in _route_track_segment_keys(route_track): + usage[segment_key] = usage.get(segment_key, 0) + 1 + return usage + + def bind_wire_task_terminals_from_payload(doc, payload): """Bind local template terminals to QET terminal UUIDs without creating wires.""" if doc is None: @@ -1427,7 +1460,10 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la missing_endpoint_uuids = set() lane_indexes_by_pair = {} lane_indexes_by_segment = {} - segment_usage_costs = {} + segment_usage_costs = _existing_routed_segment_usage( + doc, + excluded_wire_uuids=_incoming_wire_uuids(wires), + ) def add_status(status): key = str(status or "").strip() or "Unknown" diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 3787c85..bb3f2d4 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2035,6 +2035,68 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("Alternate Duct", second_labels) self.assertNotIn("Direct Duct", second_labels) + def test_route_eplan_connections_prefers_unused_segments_occupied_by_existing_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + 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( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="existing-wire", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "new-wire", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + route_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + self.assertIn("Alternate Duct", route_labels) + self.assertNotIn("Direct Duct", route_labels) + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 87d44f8ecaac8f1efe7e7489b6619121188cdd75 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 15:07:24 +0800 Subject: [PATCH 26/63] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E8=B7=AF=E5=BE=84=E5=AE=B9=E9=87=8F=E9=81=BF=E8=AE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 17 ++++- .../freecad_exchange_auto_routing_test.py | 73 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 5ac5f4a..022df06 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2278,6 +2278,19 @@ def _segment_usage_key(carrier, from_key, to_key): ) +def _carrier_capacity(carrier): + if carrier is None: + return 1 + for property_name in ("QetRouteCarrierCapacity", "QetWireCapacity"): + try: + value = int(float(getattr(carrier, property_name, 0) or 0)) + except Exception: + value = 0 + if value > 0: + return value + return 1 + + def shortest_path_with_carriers( network, start_key, @@ -2362,7 +2375,9 @@ def shortest_path_with_carriers( usage_cost = 0.0 if segment_usage_costs: usage_count = float(segment_usage_costs.get(_segment_usage_key(carrier, key, next_key), 0.0) or 0.0) - usage_cost = usage_count * float(segment_reuse_penalty or 0.0) + capacity = float(_carrier_capacity(carrier)) + excess_usage = max(usage_count - capacity + 1.0, 0.0) + usage_cost = excess_usage * float(segment_reuse_penalty or 0.0) next_state = (next_key, direction) next_cost = ( cost diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index bb3f2d4..953ce76 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2035,6 +2035,79 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("Alternate Duct", second_labels) self.assertNotIn("Direct Duct", second_labels) + def test_route_eplan_connections_respects_route_segment_capacity_before_detouring(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartC", "terminal-start-c", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndC", "terminal-end-c", app.Vector(100, 0, 0)) + direct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="Direct Duct", + project_uuid="project-1", + kind="WireDuct", + ) + direct.QetRouteCarrierCapacity = 2 + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], + label="Left Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Alternate Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + { + "wire_id": "wire-c", + "start_terminal_uuid": "terminal-start-c", + "end_terminal_uuid": "terminal-end-c", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + route_labels = [ + [segment["carrier"]["label"] for segment in route["route_track"]["segments"]] + for route in report["routes"] + ] + self.assertIn("Direct Duct", route_labels[0]) + self.assertIn("Direct Duct", route_labels[1]) + self.assertIn("Alternate Duct", route_labels[2]) + self.assertNotIn("Direct Duct", route_labels[2]) + def test_route_eplan_connections_prefers_unused_segments_occupied_by_existing_wires(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 26d227240ba3f52ece406a6179479fe30ece5c9c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 15:15:38 +0800 Subject: [PATCH 27/63] =?UTF-8?q?feat:=20=E6=9A=B4=E9=9C=B2=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E8=B7=AF=E5=BE=84=E5=AE=B9=E9=87=8F=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 20 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 17 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 022df06..aabcf90 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -409,6 +409,20 @@ def _ensure_vector_list_property(obj, prop_name, description): ) +def _ensure_integer_property(obj, prop_name, description, value): + if prop_name not in getattr(obj, "PropertiesList", []): + obj.addProperty( + "App::PropertyInteger", + prop_name, + PROPERTY_GROUP, + description, + ) + try: + setattr(obj, prop_name, int(value)) + except Exception: + setattr(obj, prop_name, 0) + + def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND): TerminalObjects.ensure_string_property( obj, @@ -438,6 +452,12 @@ def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND): "Whether routing connections can use this path", True, ) + _ensure_integer_property( + obj, + "QetRouteCarrierCapacity", + "How many routed wires can reuse this carrier segment before detouring is preferred", + 1, + ) return obj diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 953ce76..7abd8f9 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -613,6 +613,23 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual((0.65, 0.2, 1.0), terminal_access.ViewObject.LineColor) self.assertEqual("Solid", terminal_access.ViewObject.DrawStyle) + def test_route_carrier_exposes_capacity_property_for_auto_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") + + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + self.assertIn("QetRouteCarrierCapacity", carrier.PropertiesList) + self.assertEqual(1, carrier.QetRouteCarrierCapacity) + def test_route_graph_connects_crossing_carriers_at_intersection(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 6e71be95aca94a80c3480485d0248772909dfc65 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 16:20:34 +0800 Subject: [PATCH 28/63] =?UTF-8?q?feat:=20=E7=BB=A7=E6=89=BF=E7=BA=BF?= =?UTF-8?q?=E6=A7=BD=E6=BA=90=E5=AE=B9=E9=87=8F=E5=88=B0=E5=B8=83=E7=BA=BF?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 31 ++++++++++++++++--- .../freecad_exchange_auto_routing_test.py | 19 ++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index aabcf90..64216ce 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -423,7 +423,7 @@ def _ensure_integer_property(obj, prop_name, description, value): setattr(obj, prop_name, 0) -def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND): +def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): TerminalObjects.ensure_string_property( obj, "QetRoutingRole", @@ -456,11 +456,22 @@ def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND): obj, "QetRouteCarrierCapacity", "How many routed wires can reuse this carrier segment before detouring is preferred", - 1, + capacity, ) return obj +def _route_carrier_capacity_value(obj, default=1): + for property_name in ("QetRouteCarrierCapacity", "QetWireCapacity"): + try: + value = int(float(getattr(obj, property_name, 0) or 0)) + except Exception: + value = 0 + if value > 0: + return value + return int(default or 1) + + def _set_wire_duct_source_semantics(source): if source is None: return @@ -478,6 +489,12 @@ def _set_wire_duct_source_semantics(source): "How routing connection collision checks should treat this object", WIRE_DUCT_OBSTACLE_MODE, ) + _ensure_integer_property( + source, + "QetRouteCarrierCapacity", + "How many routed wires can reuse generated wire duct segments before detouring is preferred", + _route_carrier_capacity_value(source, default=1), + ) def _set_support_surface_source_semantics(source): @@ -571,7 +588,7 @@ def _create_carrier_geometry(doc, name, points): return obj -def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARRIER_KIND): +def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): """Create a routable carrier from ordered 3D points.""" if doc is None: raise RoutingNetworkError("No FreeCAD document is available.") @@ -596,7 +613,7 @@ def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARR "Ordered centerline points used by the 3D router", ) carrier.Points = list(normalized) - _set_route_carrier_semantics(carrier, project_uuid=project_uuid, kind=kind) + _set_route_carrier_semantics(carrier, project_uuid=project_uuid, kind=kind, capacity=capacity) group = WiringObjects.ensure_carrier_group(doc, project_uuid) if carrier not in getattr(group, "Group", []): @@ -1607,12 +1624,14 @@ def create_wire_duct_carriers_from_document( if len(points) < 2: continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" + capacity = _route_carrier_capacity_value(source, default=1) carrier = create_route_carrier( doc, points, label="QET Auto Wire Duct Centerline {0}".format(label), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_WIRE_DUCT, + capacity=capacity, ) _mark_wire_duct_source(source, carrier) created.append(carrier) @@ -1626,6 +1645,7 @@ def create_wire_duct_carriers_from_document( label="QET Auto Wire Duct Open End {0} {1}".format(label, end_index), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + capacity=capacity, ) ) return created @@ -1908,12 +1928,14 @@ def create_wire_duct_carriers_from_selection( if len(points) < 2: continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" + capacity = _route_carrier_capacity_value(source, default=1) carrier = create_route_carrier( doc, points, label="QET Wire Duct Centerline {0}".format(label), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_WIRE_DUCT, + capacity=capacity, ) _mark_wire_duct_source(source, carrier) created.append(carrier) @@ -1927,6 +1949,7 @@ def create_wire_duct_carriers_from_selection( label="QET Wire Duct Open End {0} {1}".format(label, end_index), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + capacity=capacity, ) ) return created diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 7abd8f9..9205f12 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1304,6 +1304,25 @@ class AutoRoutingTest(unittest.TestCase): 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_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 + + created = routing_network.create_wire_duct_carriers_from_selection( + doc, + [FakeSelectionItem(obj=duct)], + project_uuid="project-1", + margin=20.0, + ) + + 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() From 931856206593e70d8b216ee35538b9dfcd42c7a5 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 16:38:03 +0800 Subject: [PATCH 29/63] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E7=94=9F=E6=88=90=E9=80=89=E4=B8=AD=E7=BA=BF=E6=A7=BD?= =?UTF-8?q?=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 2 ++ .../freecad_exchange_auto_routing_test.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 64216ce..0a27f19 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -1916,6 +1916,8 @@ def create_wire_duct_carriers_from_selection( """Create WireDuct centerline carriers from selected duct-like solids.""" created = [] for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1): + if _live_source_carrier(doc, source) is not None: + continue bbox = _bound_box_from_object(source) if bbox is None: continue diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 9205f12..74fb726 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -916,6 +916,37 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, result["wire_duct_carriers"]) self.assertEqual("selection", result["source_mode"]) + 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") + 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() + second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + 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( + 2, + len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), + ) + 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() From ba9d971ef50a4d6dd9a9cb41238a9cc23dbcd641 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 16:52:20 +0800 Subject: [PATCH 30/63] =?UTF-8?q?feat:=20=E5=88=B7=E6=96=B0=E7=BA=BF?= =?UTF-8?q?=E6=A7=BD=E6=BA=90=E5=AF=B9=E5=BA=94=E5=B8=83=E7=BA=BF=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 204 ++++++++++++++---- .../freecad_exchange_auto_routing_test.py | 32 +++ 2 files changed, 193 insertions(+), 43 deletions(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 0a27f19..1bc4109 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -588,11 +588,7 @@ def _create_carrier_geometry(doc, name, points): return obj -def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): - """Create a routable carrier from ordered 3D points.""" - if doc is None: - raise RoutingNetworkError("No FreeCAD document is available.") - +def _normalized_route_points(points): normalized = [] for point in points or []: vector = _vector(point) @@ -600,19 +596,47 @@ def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARR continue if not normalized or _distance(normalized[-1], vector) > DEFAULT_NODE_TOLERANCE: normalized.append(vector) + return normalized - if len(normalized) < 2: - raise RoutingNetworkError("A route carrier requires at least two distinct points.") - name = _unique_name(doc, "QETRouteCarrier") - carrier = _create_carrier_geometry(doc, name, normalized) - carrier.Label = label or "QET Route Carrier" +def _set_route_carrier_points(carrier, points): _ensure_vector_list_property( carrier, "Points", "Ordered centerline points used by the 3D router", ) - carrier.Points = list(normalized) + carrier.Points = list(points) + try: + import Part + + carrier.Shape = Part.makePolygon(points) + except Exception: + pass + + +def _update_route_carrier(carrier, points, project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): + normalized = _normalized_route_points(points) + if len(normalized) < 2: + return False + _set_route_carrier_points(carrier, normalized) + _set_route_carrier_semantics(carrier, project_uuid=project_uuid, kind=kind, capacity=capacity) + _style_route_carrier(carrier, kind) + return True + + +def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): + """Create a routable carrier from ordered 3D points.""" + if doc is None: + raise RoutingNetworkError("No FreeCAD document is available.") + + normalized = _normalized_route_points(points) + if len(normalized) < 2: + raise RoutingNetworkError("A route carrier requires at least two distinct points.") + + name = _unique_name(doc, "QETRouteCarrier") + carrier = _create_carrier_geometry(doc, name, normalized) + carrier.Label = label or "QET Route Carrier" + _set_route_carrier_points(carrier, normalized) _set_route_carrier_semantics(carrier, project_uuid=project_uuid, kind=kind, capacity=capacity) group = WiringObjects.ensure_carrier_group(doc, project_uuid) @@ -1396,6 +1420,40 @@ def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_a ).get("centerline", []) +def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity=1): + carriers = _live_source_carriers(doc, source) + if not carriers: + return False + + desired = [ + (spec.get("centerline", []), ROUTE_CARRIER_KIND_WIRE_DUCT), + ] + desired.extend( + (points, ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END) + for points in (spec.get("open_ends", []) or []) + ) + + updated = [] + for carrier, desired_item in zip(carriers, desired): + points, kind = desired_item + if _update_route_carrier( + carrier, + points, + project_uuid=project_uuid, + kind=kind, + capacity=capacity, + ): + updated.append(carrier) + + if updated: + _mark_wire_duct_source(source, updated[0], updated) + try: + doc.recompute() + except Exception: + pass + return True + + def _wiring_cut_out_points_from_bbox(bbox): extents = _bbox_extents(bbox) if not extents: @@ -1436,7 +1494,57 @@ def _wire_duct_sources_from_selection(selection_ex): return sources -def _mark_wire_duct_source(source, carrier): +def _route_source_carrier_names(source): + names = [] + try: + raw = (getattr(source, "QetRouteCarrierNamesJson", "") or "").strip() + if raw: + parsed = json.loads(raw) + if isinstance(parsed, list): + names.extend(str(item).strip() for item in parsed if str(item).strip()) + except Exception: + names = [] + carrier_name = (getattr(source, "QetRouteCarrierName", "") or "").strip() + if carrier_name: + names.insert(0, carrier_name) + result = [] + seen = set() + for name in names: + if name in seen: + continue + seen.add(name) + result.append(name) + return result + + +def _live_source_carriers(doc, source): + if doc is None or source is None: + return [] + carriers = [] + for carrier_name in _route_source_carrier_names(source): + carrier = doc.getObject(carrier_name) + if carrier is not None and is_route_carrier(carrier): + carriers.append(carrier) + return carriers + + +def _remember_source_carriers(source, carriers): + live_names = [ + getattr(carrier, "Name", "") + for carrier in (carriers or []) + if carrier is not None and getattr(carrier, "Name", "") + ] + if live_names: + TerminalObjects.ensure_string_property( + source, + "QetRouteCarrierNamesJson", + PROPERTY_GROUP, + "Generated route carriers for this source", + json.dumps(live_names, ensure_ascii=False), + ) + + +def _mark_wire_duct_source(source, carrier, carriers=None): if source is None: return try: @@ -1449,6 +1557,7 @@ def _mark_wire_duct_source(source, carrier): "Generated route carrier for this source", getattr(carrier, "Name", ""), ) + _remember_source_carriers(source, carriers or ([carrier] if carrier is not None else [])) except Exception: pass @@ -1508,13 +1617,8 @@ def _mark_terminal_access_source(source, carrier): def _live_source_carrier(doc, source): - carrier_name = (getattr(source, "QetRouteCarrierName", "") or "").strip() - if not carrier_name or doc is None: - return None - carrier = doc.getObject(carrier_name) - if carrier is not None and is_route_carrier(carrier): - return carrier - return None + carriers = _live_source_carriers(doc, source) + return carriers[0] if carriers else None def detect_wire_duct_sources(doc, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT): @@ -1610,8 +1714,6 @@ def create_wire_duct_carriers_from_document( """Auto-detect wire duct objects in the document and create WireDuct centerlines.""" created = [] for index, source in enumerate(detect_wire_duct_sources(doc, min_aspect=min_aspect), start=1): - if _live_source_carrier(doc, source) is not None: - continue bbox = _bound_box_from_object(source) if bbox is None: continue @@ -1625,6 +1727,14 @@ def create_wire_duct_carriers_from_document( continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" capacity = _route_carrier_capacity_value(source, default=1) + if _sync_wire_duct_source_carriers( + doc, + source, + spec, + project_uuid=project_uuid, + capacity=capacity, + ): + continue carrier = create_route_carrier( doc, points, @@ -1633,21 +1743,22 @@ def create_wire_duct_carriers_from_document( kind=ROUTE_CARRIER_KIND_WIRE_DUCT, capacity=capacity, ) - _mark_wire_duct_source(source, carrier) + source_created = [carrier] created.append(carrier) for end_index, open_end_points in enumerate(spec.get("open_ends", []) or [], start=1): if len(open_end_points) < 2: continue - created.append( - create_route_carrier( - doc, - open_end_points, - label="QET Auto Wire Duct Open End {0} {1}".format(label, end_index), - project_uuid=project_uuid, - kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, - capacity=capacity, - ) + open_end_carrier = create_route_carrier( + doc, + open_end_points, + label="QET Auto Wire Duct Open End {0} {1}".format(label, end_index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + capacity=capacity, ) + source_created.append(open_end_carrier) + created.append(open_end_carrier) + _mark_wire_duct_source(source, carrier, source_created) return created @@ -1916,8 +2027,6 @@ def create_wire_duct_carriers_from_selection( """Create WireDuct centerline carriers from selected duct-like solids.""" created = [] for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1): - if _live_source_carrier(doc, source) is not None: - continue bbox = _bound_box_from_object(source) if bbox is None: continue @@ -1931,6 +2040,14 @@ def create_wire_duct_carriers_from_selection( continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" capacity = _route_carrier_capacity_value(source, default=1) + if _sync_wire_duct_source_carriers( + doc, + source, + spec, + project_uuid=project_uuid, + capacity=capacity, + ): + continue carrier = create_route_carrier( doc, points, @@ -1939,21 +2056,22 @@ def create_wire_duct_carriers_from_selection( kind=ROUTE_CARRIER_KIND_WIRE_DUCT, capacity=capacity, ) - _mark_wire_duct_source(source, carrier) + source_created = [carrier] created.append(carrier) for end_index, open_end_points in enumerate(spec.get("open_ends", []) or [], start=1): if len(open_end_points) < 2: continue - created.append( - create_route_carrier( - doc, - open_end_points, - label="QET Wire Duct Open End {0} {1}".format(label, end_index), - project_uuid=project_uuid, - kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, - capacity=capacity, - ) + open_end_carrier = create_route_carrier( + doc, + open_end_points, + label="QET Wire Duct Open End {0} {1}".format(label, end_index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + capacity=capacity, ) + source_created.append(open_end_carrier) + created.append(open_end_carrier) + _mark_wire_duct_source(source, carrier, source_created) return created diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 74fb726..327b2b4 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -947,6 +947,38 @@ class AutoRoutingTest(unittest.TestCase): len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), ) + 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") + 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)], + ) + + 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 + ) + + 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_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() From 66502ae505a1cc7eb1101176a380a235da5a4fb0 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 10:14:50 +0800 Subject: [PATCH 31/63] =?UTF-8?q?feat:=20=E5=88=B7=E6=96=B0=E6=94=AF?= =?UTF-8?q?=E6=92=91=E9=9D=A2=E5=B8=83=E7=BA=BF=E7=BD=91=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 21 +++++++++- .../freecad_exchange_auto_routing_test.py | 38 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 1bc4109..84ac1cc 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -1574,6 +1574,7 @@ def _mark_support_surface_source(source, carriers): "Generated route carrier for this source", getattr(carriers[0], "Name", ""), ) + _remember_source_carriers(source, carriers) except Exception: pass @@ -1797,8 +1798,6 @@ def create_surface_carriers_from_document( """Auto-detect thin support panels and create low-priority RoutingRange grids.""" created = [] for source in detect_support_surface_sources(doc): - if _live_source_carrier(doc, source) is not None: - continue bbox = _bound_box_from_object(source) if bbox is None: continue @@ -1809,6 +1808,24 @@ def create_surface_carriers_from_document( offset=offset, margin=margin, ) + live_carriers = _live_source_carriers(doc, source) + if live_carriers: + updated = [] + for carrier, points in zip(live_carriers, grids): + if _update_route_carrier( + carrier, + points, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + ): + updated.append(carrier) + if updated: + _mark_support_surface_source(source, updated) + try: + doc.recompute() + except Exception: + pass + continue source_created = [] label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Support Surface" for index, points in enumerate(grids, start=1): diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 327b2b4..a554259 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -851,6 +851,44 @@ class AutoRoutingTest(unittest.TestCase): self.assertFalse(hasattr(cabinet, "QetRoutingSourceKind")) self.assertFalse(hasattr(duct, "QetRoutingSourceKind")) + def test_auto_detect_support_surface_refreshes_routing_range_geometry(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + panel.Shape = FakeShape(FakeBoundBox(20, 140, 0, 5, 0, 100)) + created_again = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + carriers = routing_network.collect_route_carriers(doc) + x_values = [ + point.x + for carrier in carriers + if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" + for point in carrier.Points + ] + + self.assertEqual(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual(6, len([carrier for carrier in carriers if carrier.QetRouteCarrierKind == "RoutingRange"])) + self.assertEqual(20.0, min(x_values)) + self.assertEqual(140.0, max(x_values)) + def test_eplan_connection_route_can_use_auto_detected_support_surface(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From a699bc6d407052cf8b965e728f348ff882d0b511 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 11:08:22 +0800 Subject: [PATCH 32/63] =?UTF-8?q?docs:=20=E6=B2=89=E6=B7=80=E6=9C=BA?= =?UTF-8?q?=E6=9F=9C=E8=A3=85=E9=85=8D=E5=B8=83=E7=BA=BF=E5=85=B3=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 25 +++++++++++++++++-- docs/FreeCAD 机柜装配操作文档.md | 28 ++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index b16f9e9..378b70b 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -448,19 +448,40 @@ end_terminal_display 注意:批量生成布线连接的依据是导线任务,不是“所有端子自动互连”。如果文档中只有端子而没有 `wires[]` 或 `QETWiring_01_Tasks`,系统不能判断哪些端子应该连接。 +### 6.3 现场机柜资料对自动布线的约定 + +根据本地测试机柜、甲方布线操作视频和安装板/导轨/设备位置关系视频,第一版自动布线需要按真实机柜装配习惯理解对象: + +1. 柜体和框架主要提供结构边界,默认作为障碍或场景参考,不作为导线可走路径。 +2. 安装板是柜体结构的一部分,通常通过螺丝孔、加强梁或连接件固定在柜体内,不能理解为悬空对象。 +3. 安装板、背板、门板等薄板可作为低优先级 `RoutingRange` 支撑面;它们用于没有线槽或线槽不完整时的贴面过渡,不应优先于线槽。 +4. DIN 导轨固定在安装板或梁上,是设备安装基准,不是导线主路径。导轨自身不应被自动识别成线槽。 +5. 设备不能悬空,应装在导轨或安装板上。自动布线只消费设备最终 3D 位姿、工程端子位置和端子出线方向。 +6. 线槽是导线主路径。导线应优先从设备端子经 `TerminalAccess` 进入线槽,再沿 `WireDuct` 网络到达另一端。 +7. 过线孔/穿线孔用于连接不同安装面、线槽或柜体开孔处的网络,应建模为 `WiringCutOut`,不是普通障碍。 + +因此,自动布线的推荐空间语义是: + +```text +工程端子 -> TerminalAccess -> WireDuct / WiringCutOut / RoutingRange -> TerminalAccess -> 工程端子 +``` + +安装板和导轨的机械配合关系会影响对象最终位置,但当前路由器不从装配约束求解位置;它只读取 FreeCAD 文档中已经确定的几何位姿。后续如果增加装配语义,应保存在 FreeCAD 文档中,不扩展第一版数据库绑定表。 + ## 7. 当前限制 当前版本可完成布线连接原型,但仍有以下限制: 1. 线槽实体中心线生成基于包围盒长轴,不理解真实线槽开口、盖板和内部空间。 -2. 多根线会沿同一路径生成,暂未做并行错位排列。 -3. 未计算线槽填充率和容量。 +2. 多根线共路时已做基础错位显示,但不是线束级排布,也不计算每根线在线槽内的真实截面位置。 +3. 已支持简单路径容量属性和超容量避让,但未按线径、截面积、填充率计算真实线槽容量。 4. 未考虑线径、最小弯曲半径。 5. 未做强弱电分槽、线缆类型隔离。 6. 障碍检测基于 AABB,存在误报和漏报。 7. 辅助路由区域是网格近似,不等于专业软件的完整布线区域建模。 8. 端子出线方向依赖端子 LCS 方向;如果模板端子方向不准,布线连接会受影响。 9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。 +10. 当前不自动求解导轨、安装板和设备之间的 Assembly 配合关系;装配位置以 `scene.FCStd` 中对象的最终 `Placement` 为准。 ## 8. 后续需要完成 diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index 52cd607..afa1043 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -364,6 +364,34 @@ Z = 1200 mm 不要选择 `Gears`。导轨不是运动部件。 +### 7.4 现场机柜中的配合关系 + +从现场沟通和安装板/导轨/设备位置关系视频看,柜内对象不是“飘在空间里”的独立几何,而是通过机械配合关系装到柜体内: + +| 对象 | 常见宿主 | 典型配合关系 | 对布线的意义 | +| --- | --- | --- | --- | +| 安装板 | 柜体框架、背板梁、连接件 | 平面贴合、平行、距离、螺丝孔对齐 | 可作为低优先级布线支撑面 `RoutingRange` | +| DIN 导轨 | 安装板、梁 | 背面贴合、平行、距离、孔位固定 | 作为设备安装基准,不作为导线主路径 | +| 线槽 | 安装板、柜内侧边、梁 | 底面贴合、平行、距离、螺丝孔固定 | 作为导线主路径 `WireDuct` | +| 设备 | DIN 导轨或安装板 | 卡扣贴合、面贴合、孔位固定、固定间距排列 | 提供工程端子位置和出线方向 | +| 过线孔/穿线孔 | 安装板、柜体隔板 | 与开孔同轴或共面 | 作为跨区域路径 `WiringCutOut` | + +当前 FreeCADExchange 的能力边界: + +1. FreeCAD 原生 `Assembly` 工作台可以做平面对齐、距离、同轴/共线等配合关系。 +2. FreeCADExchange 目前主要保存对象最终 `Placement`,并提供轻量的 `贴合到选中面` 辅助。 +3. `贴合到选中面` 是一次性位姿调整,不是持久 Assembly 约束求解器。 +4. 当前不会自动保存“设备装在哪根导轨上”“导轨固定在哪块安装板上”这类完整宿主关系。 +5. 自动布线读取 `scene.FCStd` 里的最终几何位置,不从装配约束反推位置。 + +推荐建模习惯: + +1. 先把柜体或安装板固定好。 +2. 导轨、线槽都贴合到安装板或柜体内部结构上。 +3. 设备必须装到导轨或安装板上,避免悬空。 +4. 装配调整后重新生成布线路径网络,让 `WireDuct`、`RoutingRange` 和 `TerminalAccess` 跟随最新位置刷新。 +5. 如果后续要做自动装配,应优先在 FreeCAD 文档内增加宿主语义,例如 `Cabinet`、`MountingPlate`、`DINRail`、`WireDuct`、`Device`,不要扩展第一版数据库绑定表。 + --- ## 8. 放置线槽 From bb7cc7daf260f26f1a37300233a79d951baed288 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 11:18:18 +0800 Subject: [PATCH 33/63] =?UTF-8?q?feat:=20=E5=88=B7=E6=96=B0=E8=BF=87?= =?UTF-8?q?=E7=BA=BF=E5=AD=94=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 16 ++++++++++-- .../freecad_exchange_auto_routing_test.py | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 84ac1cc..d38ef67 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -1767,14 +1767,26 @@ def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""): """Create pass-through route carriers for wiring cut-out objects.""" created = [] for source in detect_wiring_cut_out_sources(doc): - if _live_source_carrier(doc, source) is not None: - continue bbox = _bound_box_from_object(source) if bbox is None: continue points = _wiring_cut_out_points_from_bbox(bbox) if len(points) < 2: continue + live_carrier = _live_source_carrier(doc, source) + if live_carrier is not None: + if _update_route_carrier( + live_carrier, + points, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ): + _mark_wiring_cut_out_source(source, live_carrier) + try: + doc.recompute() + except Exception: + pass + continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wiring Cut-Out" carrier = create_route_carrier( doc, diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index a554259..927880a 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1140,6 +1140,32 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, len(cut_out_carriers)) self.assertEqual("PassThrough", cut_out.QetRoutingObstacleMode) + 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() + 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)) + + 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.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, -2.0, 20.0), (70.0, 2.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) + 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() From 57fd53dec101c4fef49de813059d2bd4aa174e62 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 11:39:32 +0800 Subject: [PATCH 34/63] =?UTF-8?q?feat:=20=E5=90=8C=E6=AD=A5=E6=94=AF?= =?UTF-8?q?=E6=92=91=E9=9D=A2=E5=B8=83=E7=BA=BF=E7=BD=91=E6=A0=BC=E6=95=B0?= =?UTF-8?q?=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 24 +++++- .../freecad_exchange_auto_routing_test.py | 76 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index d38ef67..832da9f 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -1831,8 +1831,30 @@ def create_surface_carriers_from_document( kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, ): updated.append(carrier) + source_created = [] + label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Support Surface" + for index, points in enumerate(grids[len(live_carriers):], start=len(live_carriers) + 1): + if len(points) < 2: + continue + carrier = create_route_carrier( + doc, + points, + label="QET Auto Support Surface Route {0} {1}".format(label, index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + ) + source_created.append(carrier) + created.append(carrier) + for stale_carrier in live_carriers[len(grids):]: + _detach_from_groups(doc, stale_carrier) + try: + if doc.getObject(getattr(stale_carrier, "Name", "")) is not None: + doc.removeObject(stale_carrier.Name) + except Exception: + pass + current_carriers = updated + source_created if updated: - _mark_support_surface_source(source, updated) + _mark_support_surface_source(source, current_carriers) try: doc.recompute() except Exception: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 927880a..2960a64 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -889,6 +889,82 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(20.0, min(x_values)) self.assertEqual(140.0, max(x_values)) + def test_auto_detect_support_surface_adds_missing_routing_range_lanes_after_resize(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + panel.Shape = FakeShape(FakeBoundBox(0, 180, 0, 5, 0, 120)) + created_again = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" + ] + x_values = [point.x for carrier in carriers for point in carrier.Points] + z_values = [point.z for carrier in carriers for point in carrier.Points] + + self.assertEqual(6, len(created)) + self.assertEqual(1, len(created_again)) + self.assertEqual(7, len(carriers)) + self.assertEqual(180.0, max(x_values)) + self.assertEqual(120.0, max(z_values)) + + def test_auto_detect_support_surface_removes_stale_routing_range_lanes_after_resize(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + panel.Shape = FakeShape(FakeBoundBox(0, 60, 0, 5, 0, 60)) + created_again = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" + ] + x_values = [point.x for carrier in carriers for point in carrier.Points] + z_values = [point.z for carrier in carriers for point in carrier.Points] + + self.assertEqual(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual(4, len(carriers)) + self.assertEqual(60.0, max(x_values)) + self.assertEqual(60.0, max(z_values)) + def test_eplan_connection_route_can_use_auto_detected_support_surface(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 4dd33c0365918db34b19f41518d2bb092d6d5f0c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 11:56:29 +0800 Subject: [PATCH 35/63] =?UTF-8?q?feat:=20=E6=B8=85=E7=90=86=E5=A4=B1?= =?UTF-8?q?=E6=95=88=E6=BA=90=E5=AF=B9=E8=B1=A1=E5=B8=83=E7=BA=BF=E8=BD=BD?= =?UTF-8?q?=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 13 ++ src/Mod/FreeCADExchange/RoutingNetwork.py | 133 +++++++++++++++++- .../freecad_exchange_auto_routing_test.py | 55 ++++++++ 3 files changed, 198 insertions(+), 3 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 378b70b..41459a8 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -143,6 +143,16 @@ QetProjectUuid = Points = [Vector, Vector, ...] ``` +由线槽、安装板/柜面、过线孔等源对象自动生成的 carrier 还会记录来源: + +```text +QetRouteSourceName = +QetRouteSourceLabel = +QetRouteSourceKind = "WireDuct" | "RoutingRange" | "WiringCutOut" +``` + +这些属性只用于 FreeCAD 文档内部刷新和清理,不写入数据库,也不要求 QET 提供。 + carrier 统一放在: ```text @@ -324,6 +334,8 @@ src/Mod/FreeCADExchange/InitGui.py 生成线槽 carrier 时,系统除了 `WireDuct` 中心路径,还会在线槽两端生成 `WireDuctOpenEnd` 横向路径;对象名或标签包含 `Wiring Cut-Out`、`wire cutout`、`穿线孔`、`过线孔` 等语义时,会生成 `WiringCutOut` 穿线路径载体。 +自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier,并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。 + ### 5.3 布线连接功能 已完成: @@ -395,6 +407,7 @@ tests/python/freecad_exchange_auto_routing_test.py 21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 +24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 832da9f..5194266 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -22,6 +22,11 @@ ROUTE_CARRIER_KIND_WIRING_CUT_OUT = "WiringCutOut" ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath" ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange" ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess" +MANAGED_ROUTE_SOURCE_KINDS = { + ROUTE_CARRIER_KIND_WIRE_DUCT, + ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ROUTE_CARRIER_KIND_ROUTING_RANGE, +} PROPERTY_GROUP = "QET Routing" DEFAULT_NODE_TOLERANCE = 0.001 DEFAULT_SURFACE_LANE_SPACING = 100.0 @@ -872,10 +877,11 @@ def _detach_from_groups(doc, obj): pass -def clear_route_carriers(doc): - """Delete generated route carriers while keeping terminals and routed wires.""" +def _remove_route_carriers(doc, carriers): removed = 0 - for carrier in list(collect_route_carriers(doc)): + for carrier in list(carriers or []): + if carrier is None or not is_route_carrier(carrier): + continue _detach_from_groups(doc, carrier) try: if doc.getObject(getattr(carrier, "Name", "")) is not None: @@ -883,6 +889,12 @@ def clear_route_carriers(doc): removed += 1 except Exception: pass + return removed + + +def clear_route_carriers(doc): + """Delete generated route carriers while keeping terminals and routed wires.""" + removed = _remove_route_carriers(doc, collect_route_carriers(doc)) try: doc.recompute() except Exception: @@ -1528,6 +1540,40 @@ def _live_source_carriers(doc, source): return carriers +def _source_kind_value(source): + return (getattr(source, "QetRoutingSourceKind", "") or "").strip() + + +def _set_route_carrier_source_metadata(carrier, source, source_kind=""): + if carrier is None or source is None: + return + source_name = (getattr(source, "Name", "") or "").strip() + if not source_name: + return + kind = (source_kind or _source_kind_value(source)).strip() + TerminalObjects.ensure_string_property( + carrier, + "QetRouteSourceName", + PROPERTY_GROUP, + "FreeCAD source object name that generated this route carrier", + source_name, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteSourceLabel", + PROPERTY_GROUP, + "FreeCAD source object label that generated this route carrier", + getattr(source, "Label", "") or source_name, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteSourceKind", + PROPERTY_GROUP, + "Routing source kind that generated this route carrier", + kind, + ) + + def _remember_source_carriers(source, carriers): live_names = [ getattr(carrier, "Name", "") @@ -1535,6 +1581,9 @@ def _remember_source_carriers(source, carriers): if carrier is not None and getattr(carrier, "Name", "") ] if live_names: + source_kind = _source_kind_value(source) + for carrier in carriers or []: + _set_route_carrier_source_metadata(carrier, source, source_kind=source_kind) TerminalObjects.ensure_string_property( source, "QetRouteCarrierNamesJson", @@ -1591,6 +1640,7 @@ def _mark_wiring_cut_out_source(source, carrier): "Generated route carrier for this source", getattr(carrier, "Name", ""), ) + _remember_source_carriers(source, [carrier]) except Exception: pass @@ -1613,6 +1663,7 @@ def _mark_terminal_access_source(source, carrier): "Generated route carrier for this source", getattr(carrier, "Name", ""), ) + _remember_source_carriers(source, [carrier]) except Exception: pass @@ -1622,6 +1673,77 @@ def _live_source_carrier(doc, source): return carriers[0] if carriers else None +def _source_is_valid_for_kind(source, source_kind): + if source_kind == ROUTE_CARRIER_KIND_WIRE_DUCT: + return _is_wire_duct_candidate(source) + if source_kind == ROUTE_CARRIER_KIND_ROUTING_RANGE: + return _is_support_surface_candidate(source) + if source_kind == ROUTE_CARRIER_KIND_WIRING_CUT_OUT: + return _is_wiring_cut_out_candidate(source) + return True + + +def _clear_invalid_source_route_metadata(source): + for property_name in ( + "QetRouteCarrierName", + "QetRouteCarrierNamesJson", + "QetRoutingObstacleMode", + ): + if property_name not in getattr(source, "PropertiesList", []) and not getattr(source, property_name, ""): + continue + TerminalObjects.ensure_string_property( + source, + property_name, + PROPERTY_GROUP, + "Cleared invalid routing source metadata", + "", + ) + + +def _document_object_by_name(doc, name): + if doc is None or not name: + return None + try: + return doc.getObject(name) + except Exception: + return None + + +def cleanup_invalid_source_carriers(doc): + """Remove generated carriers whose FreeCAD source object is missing or invalid.""" + if doc is None: + return 0 + + removed = 0 + for carrier in list(collect_route_carriers(doc)): + source_name = (getattr(carrier, "QetRouteSourceName", "") or "").strip() + source_kind = (getattr(carrier, "QetRouteSourceKind", "") or "").strip() + if source_kind not in MANAGED_ROUTE_SOURCE_KINDS or not source_name: + continue + if _document_object_by_name(doc, source_name) is None: + removed += _remove_route_carriers(doc, [carrier]) + + for source in list(getattr(doc, "Objects", []) or []): + if source is None or is_route_carrier(source): + continue + source_kind = _source_kind_value(source) + if source_kind not in MANAGED_ROUTE_SOURCE_KINDS: + continue + if not _route_source_carrier_names(source): + continue + if _source_is_valid_for_kind(source, source_kind): + continue + removed += _remove_route_carriers(doc, _live_source_carriers(doc, source)) + _clear_invalid_source_route_metadata(source) + + if removed: + try: + doc.recompute() + except Exception: + pass + return removed + + def detect_wire_duct_sources(doc, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT): """Return document objects that look like wire ducts based on semantics/name and shape.""" sources = [] @@ -1672,6 +1794,7 @@ def prepare_layout_space_sources_from_document(doc, project_uuid=""): raise RoutingNetworkError("No FreeCAD document is available.") WiringObjects.ensure_wiring_root_group(doc, project_uuid) + cleanup_invalid_source_carriers(doc) wire_duct_sources = detect_wire_duct_sources(doc) support_surface_sources = detect_support_surface_sources(doc) @@ -1713,6 +1836,7 @@ def create_wire_duct_carriers_from_document( min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT, ): """Auto-detect wire duct objects in the document and create WireDuct centerlines.""" + cleanup_invalid_source_carriers(doc) created = [] for index, source in enumerate(detect_wire_duct_sources(doc, min_aspect=min_aspect), start=1): bbox = _bound_box_from_object(source) @@ -1765,6 +1889,7 @@ def create_wire_duct_carriers_from_document( def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""): """Create pass-through route carriers for wiring cut-out objects.""" + cleanup_invalid_source_carriers(doc) created = [] for source in detect_wiring_cut_out_sources(doc): bbox = _bound_box_from_object(source) @@ -1808,6 +1933,7 @@ def create_surface_carriers_from_document( margin=DEFAULT_SURFACE_MARGIN, ): """Auto-detect thin support panels and create low-priority RoutingRange grids.""" + cleanup_invalid_source_carriers(doc) created = [] for source in detect_support_surface_sources(doc): bbox = _bound_box_from_object(source) @@ -2076,6 +2202,7 @@ def create_wire_duct_carriers_from_selection( min_aspect=1.5, ): """Create WireDuct centerline carriers from selected duct-like solids.""" + cleanup_invalid_source_carriers(doc) created = [] for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1): bbox = _bound_box_from_object(source) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 2960a64..1be017e 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -965,6 +965,37 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(60.0, max(x_values)) self.assertEqual(60.0, max(z_values)) + def test_auto_detect_support_surface_removes_carriers_and_obstacle_mode_when_source_invalid(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 120, 0, 120)) + created_again = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + + self.assertEqual(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual([], routing_network.collect_route_carriers(doc)) + self.assertEqual("", getattr(panel, "QetRoutingObstacleMode", "")) + self.assertEqual("", getattr(panel, "QetRouteCarrierNamesJson", "")) + def test_eplan_connection_route_can_use_auto_detected_support_surface(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -1093,6 +1124,30 @@ class AutoRoutingTest(unittest.TestCase): 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_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") + 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 = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + + 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() + + self.assertEqual(3, len(generated)) + self.assertEqual([], routing_network.collect_route_carriers(doc)) + 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() From 8388f9d15ca4360de648ac8bc730c08fe0e5f47b Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 14:37:26 +0800 Subject: [PATCH 36/63] =?UTF-8?q?feat:=20=E6=89=A9=E5=B1=95=E8=BF=87?= =?UTF-8?q?=E7=BA=BF=E5=AD=94=E5=B8=83=E7=BA=BF=E6=A1=A5=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/RoutingNetwork.py | 14 ++++++-- .../freecad_exchange_auto_routing_test.py | 35 ++++++++++++++++++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 41459a8..928aa3a 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -334,6 +334,8 @@ src/Mod/FreeCADExchange/InitGui.py 生成线槽 carrier 时,系统除了 `WireDuct` 中心路径,还会在线槽两端生成 `WireDuctOpenEnd` 横向路径;对象名或标签包含 `Wiring Cut-Out`、`wire cutout`、`穿线孔`、`过线孔` 等语义时,会生成 `WiringCutOut` 穿线路径载体。 +`WiringCutOut` 不是按开孔实体外轮廓布线,而是按开孔包围盒最薄方向生成一条虚拟穿线路径,并在穿孔方向两端做小距离外扩。这样安装板、隔板或线槽侧壁上的孔可以接到孔两侧附近的线槽中心线或支撑面网格,避免路径只停留在板厚范围内而无法连通网络。 + 自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier,并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。 ### 5.3 布线连接功能 @@ -408,6 +410,7 @@ tests/python/freecad_exchange_auto_routing_test.py 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 +25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 5194266..f09f234 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -38,6 +38,7 @@ DEFAULT_ROUTE_PATH_FACE_OFFSET = 2.0 DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT = 2.5 DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE = 1000.0 DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0 +DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 WIRE_DUCT_OBSTACLE_MODE = "PassThrough" SUPPORT_SURFACE_OBSTACLE_MODE = "SupportSurface" WIRE_DUCT_NAME_KEYWORDS = ( @@ -1466,7 +1467,7 @@ def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity return True -def _wiring_cut_out_points_from_bbox(bbox): +def _wiring_cut_out_points_from_bbox(bbox, bridge_extension=0.0): extents = _bbox_extents(bbox) if not extents: return [] @@ -1482,6 +1483,9 @@ def _wiring_cut_out_points_from_bbox(bbox): fallback = max(other_extents or [DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH]) low = _axis_value(center, through_axis) - fallback * 0.5 high = _axis_value(center, through_axis) + fallback * 0.5 + extension = max(float(bridge_extension or 0.0), 0.0) + low -= extension + high += extension start = _set_axis(center, through_axis, low) end = _set_axis(center, through_axis, high) if _distance(start, end) <= DEFAULT_NODE_TOLERANCE: @@ -1887,7 +1891,11 @@ def create_wire_duct_carriers_from_document( return created -def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""): +def create_wiring_cut_out_carriers_from_document( + doc, + project_uuid="", + bridge_extension=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION, +): """Create pass-through route carriers for wiring cut-out objects.""" cleanup_invalid_source_carriers(doc) created = [] @@ -1895,7 +1903,7 @@ def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""): bbox = _bound_box_from_object(source) if bbox is None: continue - points = _wiring_cut_out_points_from_bbox(bbox) + points = _wiring_cut_out_points_from_bbox(bbox, bridge_extension=bridge_extension) if len(points) < 2: continue live_carrier = _live_source_carrier(doc, source) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 1be017e..317ffe7 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1295,7 +1295,40 @@ class AutoRoutingTest(unittest.TestCase): 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, -2.0, 20.0), (70.0, 2.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) + 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_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") + 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", + ) + 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) + + self.assertEqual("Routed", result["route_status"]) + self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"]) + self.assertEqual(0, result["collision_count"]) def test_check_routing_path_network_writes_diagnostic_for_unconnected_terminal(self): _install_fake_freecad() From 90a3d1786e7cbdd2e304c3dda2a6114c8903134c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 14:43:00 +0800 Subject: [PATCH 37/63] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=BF=87?= =?UTF-8?q?=E7=BA=BF=E5=AD=94=E6=A1=A5=E6=8E=A5=E8=B7=9D=E7=A6=BB=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 10 ++++- src/Mod/FreeCADExchange/RoutingNetwork.py | 43 ++++++++++++++++--- .../freecad_exchange_auto_routing_test.py | 25 +++++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 928aa3a..f759dff 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -336,6 +336,14 @@ src/Mod/FreeCADExchange/InitGui.py `WiringCutOut` 不是按开孔实体外轮廓布线,而是按开孔包围盒最薄方向生成一条虚拟穿线路径,并在穿孔方向两端做小距离外扩。这样安装板、隔板或线槽侧壁上的孔可以接到孔两侧附近的线槽中心线或支撑面网格,避免路径只停留在板厚范围内而无法连通网络。 +过线孔源对象可通过下面的 FreeCAD 属性调整外扩距离: + +```text +QetWiringCutOutBridgeExtensionMm = 20.0 +``` + +该属性表示在穿孔方向每一侧额外延长多少毫米。默认值用于常见线槽中心线与板面/孔位有少量间距的情况;如果现场模型中线槽中心离开孔更近或更远,可以在 FreeCAD 属性面板里按对象单独调整。 + 自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier,并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。 ### 5.3 布线连接功能 @@ -410,7 +418,7 @@ tests/python/freecad_exchange_auto_routing_test.py 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 -25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络。 +25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络,并支持通过 `QetWiringCutOutBridgeExtensionMm` 按对象调整外扩距离。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index f09f234..2bb141f 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -429,6 +429,30 @@ def _ensure_integer_property(obj, prop_name, description, value): setattr(obj, prop_name, 0) +def _ensure_float_property(obj, prop_name, description, value): + if prop_name not in getattr(obj, "PropertiesList", []): + obj.addProperty( + "App::PropertyFloat", + prop_name, + PROPERTY_GROUP, + description, + ) + try: + setattr(obj, prop_name, float(value)) + except Exception: + setattr(obj, prop_name, 0.0) + + +def _wiring_cut_out_bridge_extension_value(source, default=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION): + try: + value = float(getattr(source, "QetWiringCutOutBridgeExtensionMm", default) or 0.0) + except Exception: + value = float(default or 0.0) + if value < 0.0: + return 0.0 + return value + + def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): TerminalObjects.ensure_string_property( obj, @@ -522,7 +546,7 @@ def _set_support_surface_source_semantics(source): ) -def _set_wiring_cut_out_source_semantics(source): +def _set_wiring_cut_out_source_semantics(source, bridge_extension=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION): if source is None: return TerminalObjects.ensure_string_property( @@ -539,6 +563,12 @@ def _set_wiring_cut_out_source_semantics(source): "How routing connection collision checks should treat this object", WIRE_DUCT_OBSTACLE_MODE, ) + _ensure_float_property( + source, + "QetWiringCutOutBridgeExtensionMm", + "How far the generated wiring cut-out carrier extends beyond each side of the opening", + _wiring_cut_out_bridge_extension_value(source, default=bridge_extension), + ) def _style_route_carrier(carrier, kind): @@ -1632,11 +1662,11 @@ def _mark_support_surface_source(source, carriers): pass -def _mark_wiring_cut_out_source(source, carrier): +def _mark_wiring_cut_out_source(source, carrier, bridge_extension=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION): if source is None or carrier is None: return try: - _set_wiring_cut_out_source_semantics(source) + _set_wiring_cut_out_source_semantics(source, bridge_extension=bridge_extension) TerminalObjects.ensure_string_property( source, "QetRouteCarrierName", @@ -1903,7 +1933,8 @@ def create_wiring_cut_out_carriers_from_document( bbox = _bound_box_from_object(source) if bbox is None: continue - points = _wiring_cut_out_points_from_bbox(bbox, bridge_extension=bridge_extension) + source_bridge_extension = _wiring_cut_out_bridge_extension_value(source, default=bridge_extension) + points = _wiring_cut_out_points_from_bbox(bbox, bridge_extension=source_bridge_extension) if len(points) < 2: continue live_carrier = _live_source_carrier(doc, source) @@ -1914,7 +1945,7 @@ def create_wiring_cut_out_carriers_from_document( project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_WIRING_CUT_OUT, ): - _mark_wiring_cut_out_source(source, live_carrier) + _mark_wiring_cut_out_source(source, live_carrier, bridge_extension=source_bridge_extension) try: doc.recompute() except Exception: @@ -1928,7 +1959,7 @@ def create_wiring_cut_out_carriers_from_document( project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_WIRING_CUT_OUT, ) - _mark_wiring_cut_out_source(source, carrier) + _mark_wiring_cut_out_source(source, carrier, bridge_extension=source_bridge_extension) created.append(carrier) return created diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 317ffe7..a52709b 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1297,6 +1297,31 @@ class AutoRoutingTest(unittest.TestCase): 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_wiring_cut_out_source_bridge_extension_controls_generated_path_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") + 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 + + 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.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_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() From ea0e0a701d059b740f6844d789acdcd74d9f0af0 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 14:50:28 +0800 Subject: [PATCH 38/63] =?UTF-8?q?feat:=20=E8=AE=B0=E5=BD=95=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E8=BD=A8=E8=BF=B9=E6=BA=90=E5=AF=B9=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/RoutingNetwork.py | 12 +++++++- .../freecad_exchange_auto_routing_test.py | 28 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index f759dff..d7bd885 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -346,6 +346,8 @@ QetWiringCutOutBridgeExtensionMm = 20.0 自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier,并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。 +生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象,route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。 + ### 5.3 布线连接功能 已完成: @@ -419,6 +421,7 @@ tests/python/freecad_exchange_auto_routing_test.py 23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络,并支持通过 `QetWiringCutOutBridgeExtensionMm` 按对象调整外扩距离。 +26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 2bb141f..bbe6427 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2643,11 +2643,21 @@ def connect_point_to_network(network, point): def _carrier_track_payload(carrier): - return { + payload = { "name": getattr(carrier, "Name", ""), "label": getattr(carrier, "Label", ""), "kind": (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND, } + source_fields = ( + ("source_name", "QetRouteSourceName"), + ("source_label", "QetRouteSourceLabel"), + ("source_kind", "QetRouteSourceKind"), + ) + for payload_key, property_name in source_fields: + value = (getattr(carrier, property_name, "") or "").strip() + if value: + payload[payload_key] = value + return payload def _segment_usage_key(carrier, from_key, to_key): diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index a52709b..b22b573 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -281,6 +281,34 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("WireDuct", payload["route_track"]["segments"][0]["carrier"]["kind"]) self.assertTrue(json.loads(wire.QetRouteTrackJson)["carrier_names"]) + def test_route_track_preserves_generated_carrier_source_metadata(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)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "线槽A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + + auto_routing_panel.AutoRoutingController().generate_routing_paths() + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + route_track = json.loads(result["wire"].QetRouteTrackJson) + wire_duct_carriers = [ + segment["carrier"] + for segment in route_track["segments"] + if segment["carrier"]["kind"] == "WireDuct" + ] + + self.assertTrue(wire_duct_carriers) + self.assertEqual("WireDuctA", wire_duct_carriers[0].get("source_name")) + self.assertEqual("线槽A", wire_duct_carriers[0].get("source_label")) + self.assertEqual("WireDuct", wire_duct_carriers[0].get("source_kind")) + def test_network_eplan_connection_route_offsets_lane_by_route_index(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From f653549366e457022ccd549c5c063f9c317e09b4 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 15:08:21 +0800 Subject: [PATCH 39/63] =?UTF-8?q?feat:=20=E5=B1=95=E7=A4=BA=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E8=B7=AF=E5=BE=84=E6=BA=90=E5=AF=B9=E8=B1=A1=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/AutoRouting.py | 39 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 26 +++++++++++++ 3 files changed, 68 insertions(+) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index d7bd885..50ac511 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -348,6 +348,8 @@ QetWiringCutOutBridgeExtensionMm = 20.0 生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象,route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。 +批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。 + ### 5.3 布线连接功能 已完成: @@ -422,6 +424,7 @@ tests/python/freecad_exchange_auto_routing_test.py 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络,并支持通过 `QetWiringCutOutBridgeExtensionMm` 按对象调整外扩距离。 26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 +27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 08ec063..a5836f3 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1689,6 +1689,42 @@ def _endpoint_pair_text(sample): ) +def _route_source_labels(route_track, limit=5): + labels = [] + seen = set() + if not isinstance(route_track, dict): + return labels + for segment in route_track.get("segments", []) or []: + carrier = segment.get("carrier", {}) if isinstance(segment, dict) else {} + if not isinstance(carrier, dict): + continue + label = ( + str(carrier.get("source_label", "") or "").strip() + or str(carrier.get("source_name", "") or "").strip() + ) + if not label or label in seen: + continue + seen.add(label) + labels.append(label) + if len(labels) >= int(limit or 0): + break + return labels + + +def _route_source_sample_text(report): + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + labels = _route_source_labels(route.get("route_track", {})) + if not labels: + continue + return "路径示例:导线 {0} 经过 {1}。".format( + _wire_sample_text(route), + "、".join(labels), + ) + return "" + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1731,6 +1767,9 @@ def format_eplan_connection_route_report(report): total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) if total_length_mm > 0.0: message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) + route_source_sample = _route_source_sample_text(report) + if route_source_sample: + message += "\n{0}".format(route_source_sample) errors = report.get("errors", []) or [] if errors: message += "\n首个错误:{0}".format(str(errors[0])) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index b22b573..1cf7c64 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1941,6 +1941,32 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"]) self.assertIn("总长度", message) + def test_route_report_includes_route_source_sample_when_available(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_label": "N4111", + "route_track": { + "segments": [ + {"carrier": {"kind": "TerminalAccess", "source_label": "QF1:A1"}}, + {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + {"carrier": {"kind": "WiringCutOut", "source_label": "过线孔A"}}, + {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message) + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 2019f54051fa86023ca50df73ca6180960010c7f Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 15:15:32 +0800 Subject: [PATCH 40/63] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BA=BF?= =?UTF-8?q?=E6=A7=BD=E7=AB=AF=E9=83=A8=E7=BC=A9=E8=BF=9B=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 4 ++ src/Mod/FreeCADExchange/RoutingNetwork.py | 45 +++++++++++++++---- .../freecad_exchange_auto_routing_test.py | 20 +++++++++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 50ac511..2965f04 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -195,8 +195,11 @@ QetAutoRouteDiagnosticsJson QetRoutingSourceKind = "WireDuct" QetRoutingObstacleMode = "PassThrough" QetRouteCarrierName = +QetWireDuctEndMarginMm = 20.0 ``` +`QetWireDuctEndMarginMm` 表示自动生成的线槽中心线距离线槽两端缩进多少毫米。默认值用于避开线槽端盖/端部开口;如果某个线槽很短,或现场希望中心路径更靠近端部,可以在 FreeCAD 属性面板中按对象调整。 + ## 4. 算法设计 ### 4.1 路由网络构建 @@ -425,6 +428,7 @@ tests/python/freecad_exchange_auto_routing_test.py 25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络,并支持通过 `QetWiringCutOutBridgeExtensionMm` 按对象调整外扩距离。 26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 +28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index bbe6427..26da3be 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -502,7 +502,17 @@ def _route_carrier_capacity_value(obj, default=1): return int(default or 1) -def _set_wire_duct_source_semantics(source): +def _wire_duct_end_margin_value(source, default=DEFAULT_WIRE_DUCT_MARGIN): + try: + value = float(getattr(source, "QetWireDuctEndMarginMm", default) or 0.0) + except Exception: + value = float(default or 0.0) + if value < 0.0: + return 0.0 + return value + + +def _set_wire_duct_source_semantics(source, end_margin=DEFAULT_WIRE_DUCT_MARGIN): if source is None: return TerminalObjects.ensure_string_property( @@ -525,6 +535,12 @@ def _set_wire_duct_source_semantics(source): "How many routed wires can reuse generated wire duct segments before detouring is preferred", _route_carrier_capacity_value(source, default=1), ) + _ensure_float_property( + source, + "QetWireDuctEndMarginMm", + "How far generated wire duct centerlines stay inside each duct end", + _wire_duct_end_margin_value(source, default=end_margin), + ) def _set_support_surface_source_semantics(source): @@ -1463,7 +1479,14 @@ def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_a ).get("centerline", []) -def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity=1): +def _sync_wire_duct_source_carriers( + doc, + source, + spec, + project_uuid="", + capacity=1, + end_margin=DEFAULT_WIRE_DUCT_MARGIN, +): carriers = _live_source_carriers(doc, source) if not carriers: return False @@ -1489,7 +1512,7 @@ def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity updated.append(carrier) if updated: - _mark_wire_duct_source(source, updated[0], updated) + _mark_wire_duct_source(source, updated[0], updated, end_margin=end_margin) try: doc.recompute() except Exception: @@ -1627,11 +1650,11 @@ def _remember_source_carriers(source, carriers): ) -def _mark_wire_duct_source(source, carrier, carriers=None): +def _mark_wire_duct_source(source, carrier, carriers=None, end_margin=DEFAULT_WIRE_DUCT_MARGIN): if source is None: return try: - _set_wire_duct_source_semantics(source) + _set_wire_duct_source_semantics(source, end_margin=end_margin) if carrier is not None: TerminalObjects.ensure_string_property( source, @@ -1876,9 +1899,10 @@ def create_wire_duct_carriers_from_document( bbox = _bound_box_from_object(source) if bbox is None: continue + source_margin = _wire_duct_end_margin_value(source, default=margin) spec = _wire_duct_centerline_spec_from_bbox( bbox, - margin=margin, + margin=source_margin, min_aspect=min_aspect, ) points = spec.get("centerline", []) @@ -1892,6 +1916,7 @@ def create_wire_duct_carriers_from_document( spec, project_uuid=project_uuid, capacity=capacity, + end_margin=source_margin, ): continue carrier = create_route_carrier( @@ -1917,7 +1942,7 @@ def create_wire_duct_carriers_from_document( ) source_created.append(open_end_carrier) created.append(open_end_carrier) - _mark_wire_duct_source(source, carrier, source_created) + _mark_wire_duct_source(source, carrier, source_created, end_margin=source_margin) return created @@ -2247,9 +2272,10 @@ def create_wire_duct_carriers_from_selection( bbox = _bound_box_from_object(source) if bbox is None: continue + source_margin = _wire_duct_end_margin_value(source, default=margin) spec = _wire_duct_centerline_spec_from_bbox( bbox, - margin=margin, + margin=source_margin, min_aspect=min_aspect, ) points = spec.get("centerline", []) @@ -2263,6 +2289,7 @@ def create_wire_duct_carriers_from_selection( spec, project_uuid=project_uuid, capacity=capacity, + end_margin=source_margin, ): continue carrier = create_route_carrier( @@ -2288,7 +2315,7 @@ def create_wire_duct_carriers_from_selection( ) source_created.append(open_end_carrier) created.append(open_end_carrier) - _mark_wire_duct_source(source, carrier, source_created) + _mark_wire_duct_source(source, carrier, source_created, end_margin=source_margin) return created diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 1cf7c64..b2ab08d 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1648,6 +1648,26 @@ class AutoRoutingTest(unittest.TestCase): 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_wire_duct_source_end_margin_controls_generated_centerline_length(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)) + 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() From 173580594cb0a0491eb74ddcb95fa306ae4a58ab Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 15:51:34 +0800 Subject: [PATCH 41/63] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=9B=B8?= =?UTF-8?q?=E9=82=BB=E7=BA=BF=E6=A7=BD=E6=A1=A5=E6=8E=A5=E5=AE=B9=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/AutoRouting.py | 13 ++++++-- .../freecad_exchange_auto_routing_test.py | 31 +++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 2965f04..541ac09 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -85,6 +85,8 @@ terminal_uuid 构图时不要求所有 carrier 都提前手工打断。系统会识别轴向线段之间的几何相交和同线重叠,把交点/重叠端点自动切成图节点。这样多条线槽中心路径只要在空间中相交,就可以在交点处换向,Dijkstra 才能得到符合工程布线习惯的折线路径。 +相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝。 + ### 2.1 路由优先级 当前版本按下面优先级处理: @@ -429,6 +431,7 @@ tests/python/freecad_exchange_auto_routing_test.py 26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 +29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index a5836f3..d77ea17 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -29,6 +29,7 @@ DEFAULT_OPTIONS = { # 线槽网络相关参数。 "use_routing_network": True, "network_entry_max_distance": 1000.0, + "adjoining_duct_tolerance": RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, "bend_penalty": 25.0, # EPLAN/SOLIDWORKS 风格:线槽/路由路径最优先,辅助面域只作为过渡/兜底区域。 "carrier_kind_cost_factors": { @@ -824,6 +825,7 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "network": { "carriers": int(network.get("carrier_count", 0)), "segments": int(network.get("segment_count", 0)), + "bridged_segments": int(network.get("bridged_segment_count", 0)), "blocked_segments": int(network.get("blocked_segment_count", 0)), "nodes": len(network.get("nodes", {})), "entry_distance": float(start_distance or 0.0), @@ -843,12 +845,19 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non blocked_bboxes = [obstacle["bbox"] for obstacle in obstacles if obstacle.get("bbox")] if blocked_bboxes: - obstacle_aware_network = RoutingNetwork.build_route_graph(doc, blocked_bboxes=blocked_bboxes) + obstacle_aware_network = RoutingNetwork.build_route_graph( + doc, + blocked_bboxes=blocked_bboxes, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) route_data = route_on_network(obstacle_aware_network, obstacle_aware=True) if route_data is not None: return route_data - network = RoutingNetwork.build_route_graph(doc) + network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) return route_on_network(network, obstacle_aware=False) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index b2ab08d..bc9283e 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -743,6 +743,37 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) + def test_auto_routing_respects_adjoining_duct_tolerance_option(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(44, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"adjoining_duct_tolerance": 15.0}, + ) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(1, result["network"]["bridged_segments"]) + def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self): _install_fake_freecad() _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() From d3b38dc70ac12921e760f2745174faf709c5c057 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:04:02 +0800 Subject: [PATCH 42/63] =?UTF-8?q?feat:=20=E9=9D=A2=E6=9D=BF=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=BA=BF=E6=A7=BD=E6=A1=A5=E6=8E=A5=E5=AE=B9=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/AutoRoutingPanel.py | 39 +++++++++++++++++- .../freecad_exchange_auto_routing_test.py | 40 +++++++++++++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 541ac09..69fe0ef 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -87,6 +87,8 @@ terminal_uuid 相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝。 +FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。 + ### 2.1 路由优先级 当前版本按下面优先级处理: @@ -432,6 +434,7 @@ tests/python/freecad_exchange_auto_routing_test.py 27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。 +30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index e5e3906..5fd190d 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -71,8 +71,19 @@ def _selection_ex(): class AutoRoutingController: - def __init__(self): + def __init__(self, options=None): self.last_report = None + self.options = dict(options or {}) + + def routing_options(self): + return dict(self.options) + + def set_adjoining_duct_tolerance(self, value): + try: + tolerance = float(value) + except Exception: + tolerance = RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE + self.options["adjoining_duct_tolerance"] = max(tolerance, 0.0) def summary(self): doc = _active_document() @@ -109,6 +120,7 @@ class AutoRoutingController: self.last_report = AutoRouting.generate_eplan_routing_path_network( doc, project_uuid=project_uuid, + options=self.routing_options(), selection_ex=selection_ex, ) self.last_report["source_mode"] = source_mode @@ -120,6 +132,7 @@ class AutoRoutingController: self.last_report = AutoRouting.check_eplan_routing_path_network( doc, project_uuid=project_uuid, + options=self.routing_options(), ) return self.last_report @@ -141,6 +154,7 @@ class AutoRoutingController: report = AutoRouting.route_eplan_connections( doc, payload=payload if isinstance(payload, dict) and payload.get("wires") else None, + options=self.routing_options(), project_uuid=project_uuid, update_network=True, ) @@ -183,6 +197,22 @@ class AutoRoutingTaskPanel: layout = QtWidgets.QVBoxLayout(self.form) + options_layout = QtWidgets.QHBoxLayout() + options_layout.addWidget(QtWidgets.QLabel("线槽桥接容差 mm")) + self.adjoining_duct_tolerance_spin = QtWidgets.QDoubleSpinBox() + self.adjoining_duct_tolerance_spin.setRange(0.0, 1000.0) + self.adjoining_duct_tolerance_spin.setDecimals(1) + self.adjoining_duct_tolerance_spin.setSingleStep(1.0) + self.adjoining_duct_tolerance_spin.setValue( + float( + self.controller.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + ) + ) + options_layout.addWidget(self.adjoining_duct_tolerance_spin) + self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间") self.generate_layout_button.setToolTip( "按 EPLAN 布局空间语义识别线槽、安装面、工程端子和障碍处理方式,不生成导线。" @@ -221,6 +251,7 @@ class AutoRoutingTaskPanel: ): layout.addWidget(widget) + layout.addLayout(options_layout) layout.addWidget(self.status_label) self.generate_paths_button.clicked.connect(self.generate_routing_paths) @@ -247,8 +278,12 @@ class AutoRoutingTaskPanel: self.status_label.setText(message) _console_error(message) + def _sync_options_from_widgets(self): + self.controller.set_adjoining_duct_tolerance(self.adjoining_duct_tolerance_spin.value()) + def generate_routing_paths(self): try: + self._sync_options_from_widgets() result = self.controller.generate_routing_paths() wire_ducts = result.get("wire_duct_carriers", 0) surfaces = result.get("surface_carriers", 0) @@ -274,6 +309,7 @@ class AutoRoutingTaskPanel: def check_routing_path_network(self): try: + self._sync_options_from_widgets() result = self.controller.check_routing_path_network() diagnostic = result.get("diagnostic", {}) if isinstance(result.get("diagnostic", {}), dict) else {} self._set_status( @@ -310,6 +346,7 @@ class AutoRoutingTaskPanel: def route_eplan_connections(self): try: + self._sync_options_from_widgets() report = self.controller.route_eplan_connections() self._set_status(AutoRouting.format_eplan_connection_route_report(report)) except Exception as exc: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index bc9283e..8dc20be 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1538,6 +1538,46 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, diagnostic_payload["prepared_layout"]["wire_duct_carriers"]) self.assertEqual(2, diagnostic_payload["prepared_layout"]["terminal_access_carriers"]) + 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") + _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(44, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(56, 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-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() + + self.assertEqual(1, report["routed"]) + self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) + 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() From e3029f2bd09e9e99cc062dc220f9aa5d74ca3815 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:15:23 +0800 Subject: [PATCH 43/63] =?UTF-8?q?feat:=20=E9=9D=A2=E6=9D=BF=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=AB=AF=E5=AD=90=E6=8E=A5=E5=85=A5=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 19 +++++-- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 44 +++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 49 +++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 69fe0ef..429d3b3 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -89,6 +89,8 @@ terminal_uuid FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。 +同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 + ### 2.1 路由优先级 当前版本按下面优先级处理: @@ -270,6 +272,15 @@ src/Mod/FreeCADExchange/AutoRouting.py network-dijkstra-v1 ``` +端子接入有两个可调参数: + +```text +terminal_exit_length = 20.0 +terminal_access_max_distance = 1000.0 +``` + +`terminal_exit_length` 决定端子出线段长度;`terminal_access_max_distance` 决定端子出线点到最近路由网络的最大允许接入距离。两个参数都只保存在当前 FreeCAD 面板/调用选项中,不写数据库。 + ### 4.4 悬空线策略 当前版本默认: @@ -435,6 +446,7 @@ tests/python/freecad_exchange_auto_routing_test.py 28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。 30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值。 +31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 已完成 FreeCAD smoke: @@ -454,9 +466,10 @@ tests/manual/freecad_auto_routing_smoke.py 3. 清除布线连接 4. 清除走线路径 5. 点击“准备布线布局空间” -6. 可选:选中无法自动识别的线槽实体 -7. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别 -8. 点击“生成布线连接” +6. 按当前机柜情况调整线槽桥接容差、端子接入最大距离、端子出线长度 +7. 可选:选中无法自动识别的线槽实体 +8. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别 +9. 点击“生成布线连接” ``` 三个按钮的职责: diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 5fd190d..d6a6b8d 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -85,6 +85,20 @@ class AutoRoutingController: tolerance = RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE self.options["adjoining_duct_tolerance"] = max(tolerance, 0.0) + def set_terminal_access_max_distance(self, value): + try: + max_distance = float(value) + except Exception: + max_distance = RoutingNetwork.DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE + self.options["terminal_access_max_distance"] = max(max_distance, 0.0) + + def set_terminal_exit_length(self, value): + try: + exit_length = float(value) + except Exception: + exit_length = AutoRouting.DEFAULT_OPTIONS["terminal_exit_length"] + self.options["terminal_exit_length"] = max(exit_length, 0.0) + def summary(self): doc = _active_document() terminal_count = len(AutoRouting.index_terminals(doc)) @@ -212,6 +226,34 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.adjoining_duct_tolerance_spin) + options_layout.addWidget(QtWidgets.QLabel("端子接入最大距离 mm")) + self.terminal_access_max_distance_spin = QtWidgets.QDoubleSpinBox() + self.terminal_access_max_distance_spin.setRange(0.0, 100000.0) + self.terminal_access_max_distance_spin.setDecimals(1) + self.terminal_access_max_distance_spin.setSingleStep(50.0) + self.terminal_access_max_distance_spin.setValue( + float( + self.controller.routing_options().get( + "terminal_access_max_distance", + RoutingNetwork.DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, + ) + ) + ) + options_layout.addWidget(self.terminal_access_max_distance_spin) + options_layout.addWidget(QtWidgets.QLabel("端子出线长度 mm")) + self.terminal_exit_length_spin = QtWidgets.QDoubleSpinBox() + self.terminal_exit_length_spin.setRange(0.0, 1000.0) + self.terminal_exit_length_spin.setDecimals(1) + self.terminal_exit_length_spin.setSingleStep(5.0) + self.terminal_exit_length_spin.setValue( + float( + self.controller.routing_options().get( + "terminal_exit_length", + AutoRouting.DEFAULT_OPTIONS["terminal_exit_length"], + ) + ) + ) + options_layout.addWidget(self.terminal_exit_length_spin) self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间") self.generate_layout_button.setToolTip( @@ -280,6 +322,8 @@ class AutoRoutingTaskPanel: def _sync_options_from_widgets(self): self.controller.set_adjoining_duct_tolerance(self.adjoining_duct_tolerance_spin.value()) + self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value()) + self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value()) def generate_routing_paths(self): try: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 8dc20be..5276844 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1499,6 +1499,55 @@ class AutoRoutingTest(unittest.TestCase): 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() + 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)) + + controller = auto_routing_panel.AutoRoutingController() + controller.set_terminal_access_max_distance(6000.0) + result = controller.generate_routing_paths() + + 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_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")), + ) + self.assertEqual(40.0, controller.routing_options()["terminal_exit_length"]) + 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() From ad24e9669580af091fc5d6e35014cb45a28a2d20 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:29:05 +0800 Subject: [PATCH 44/63] =?UTF-8?q?fix:=20=E6=A3=80=E6=9F=A5=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E7=BD=91=E7=BB=9C=E4=BD=BF=E7=94=A8=E6=A1=A5=E6=8E=A5?= =?UTF-8?q?=E5=AE=B9=E5=B7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 4 +- src/Mod/FreeCADExchange/AutoRouting.py | 8 ++- src/Mod/FreeCADExchange/RoutingNetwork.py | 15 ++++-- .../freecad_exchange_auto_routing_test.py | 52 +++++++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 429d3b3..ade4a06 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -87,7 +87,7 @@ terminal_uuid 相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝。 -FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。 +FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。检查通过时,如果存在相邻线槽自动桥接,报告会显示自动桥接段数,便于确认当前容差是否生效。 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 @@ -445,7 +445,7 @@ tests/python/freecad_exchange_auto_routing_test.py 27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。 -30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值。 +30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index d77ea17..a6125b4 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1980,6 +1980,7 @@ def generate_eplan_routing_path_network(doc, project_uuid="", options=None, sele selection_ex=selection_ex, 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), + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), ) @@ -1999,6 +2000,7 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None): project_uuid=target_project_uuid, 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), + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), ) diagnostic = result.get("diagnostic", {}) if isinstance(result, dict) else {} return { @@ -2054,11 +2056,15 @@ def format_routing_path_network_report(diagnostic): summary = diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {} issues = _dict_items(diagnostic.get("issues", []) or []) if not issues: - return "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。".format( + message = "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。".format( summary.get("carriers", 0), summary.get("segments", 0), summary.get("nodes", 0), ) + bridged_segments = int(summary.get("bridged_segments", 0) or 0) + if bridged_segments > 0: + message += " 自动桥接 {0} 段相邻线槽。".format(bridged_segments) + return message message = "布线路径网络检查发现 {0} 类问题。".format(len(issues)) unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or []) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 26da3be..c3ea4cb 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2193,6 +2193,7 @@ def create_routing_path_network_from_document( selection_ex=None, terminal_exit_length=20.0, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, + adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): """Generate the EPLAN-style routing path network for the layout space. @@ -2254,7 +2255,10 @@ def create_routing_path_network_from_document( "surface_carriers": len(surfaces), "terminal_access_carriers": len(terminal_access), "layout_space": layout_space, - "network": network_summary(doc), + "network": network_summary( + doc, + adjoining_duct_tolerance=adjoining_duct_tolerance, + ), } @@ -2820,8 +2824,8 @@ def path_points(network, path_keys): return [nodes[key] for key in path_keys or [] if key in nodes] -def network_summary(doc): - network = build_route_graph(doc) +def network_summary(doc, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE): + network = build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance) return _network_summary_from_graph(network) @@ -2927,12 +2931,13 @@ def diagnose_routing_path_network( doc, terminal_exit_length=20.0, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, + adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): """Inspect the generated routing path network without routing wires.""" if doc is None: raise RoutingNetworkError("No FreeCAD document is available.") - network = build_route_graph(doc) + network = build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance) components = _route_graph_components(network) summary = _network_summary_from_graph(network) isolated_components = components if len(components) > 1 else [] @@ -3053,11 +3058,13 @@ def write_routing_path_network_diagnostic( project_uuid="", terminal_exit_length=20.0, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, + adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): diagnostic = diagnose_routing_path_network( doc, terminal_exit_length=terminal_exit_length, terminal_access_max_distance=terminal_access_max_distance, + adjoining_duct_tolerance=adjoining_duct_tolerance, ) group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) _clear_routing_path_network_diagnostics(doc, group) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 5276844..e389535 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1480,6 +1480,58 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("(0.0, 0.0, 20.0)", message) 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_check_routing_path_network_uses_adjoining_duct_tolerance_option(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + for index, 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, + project_uuid="project-1", + options={"adjoining_duct_tolerance": 15.0}, + ) + + 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() From cbd898f2ecd73e8036bab14524e4ddb164d66586 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:32:29 +0800 Subject: [PATCH 45/63] =?UTF-8?q?feat:=20=E7=AB=AF=E5=AD=90=E6=9C=AA?= =?UTF-8?q?=E6=8E=A5=E5=85=A5=E8=AF=8A=E6=96=AD=E6=98=BE=E7=A4=BA=E9=98=88?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 +++ src/Mod/FreeCADExchange/AutoRouting.py | 3 ++- src/Mod/FreeCADExchange/RoutingNetwork.py | 2 ++ tests/python/freecad_exchange_auto_routing_test.py | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index ade4a06..b65a3de 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -281,6 +281,8 @@ terminal_access_max_distance = 1000.0 `terminal_exit_length` 决定端子出线段长度;`terminal_access_max_distance` 决定端子出线点到最近路由网络的最大允许接入距离。两个参数都只保存在当前 FreeCAD 面板/调用选项中,不写数据库。 +网络检查发现端子未接入时,诊断 JSON 会记录该端子到最近路由网络的距离、当前端子接入最大距离和端子出线长度;面板报告会显示当前最大接入距离,便于判断是设备/线槽位置还没摆好,还是需要临时调大接入阈值。 + ### 4.4 悬空线策略 当前版本默认: @@ -447,6 +449,7 @@ tests/python/freecad_exchange_auto_routing_test.py 29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。 30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 +32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index a6125b4..26fdf9a 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -2070,9 +2070,10 @@ def format_routing_path_network_report(diagnostic): unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or []) if unconnected: sample = unconnected[0] - message += "\n端子未接入:{0},距离最近网络 {1}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format( + message += "\n端子未接入:{0},距离最近网络 {1},当前端子接入最大距离 {2}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format( _diagnostic_terminal_text(sample), _format_distance_mm(sample.get("nearest_network_distance_mm")), + _format_distance_mm(sample.get("terminal_access_max_distance_mm")), ) possible_breaks = _dict_items(diagnostic.get("possible_breaks", []) or []) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index c3ea4cb..c7af304 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2959,6 +2959,8 @@ def diagnose_routing_path_network( "access_carrier": getattr(access_carrier, "Name", "") if access_carrier is not None else "", "nearest_network_distance_mm": None if distance is None else float(distance), "nearest_network_point": None if nearest_point is None else _point_payload(nearest_point), + "terminal_access_max_distance_mm": float(max_distance), + "terminal_exit_length_mm": float(max(float(terminal_exit_length or 0.0), 0.0)), "code": "terminal_access_missing" if not access_live else "terminal_access_too_far", } ) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index e389535..8ce07ec 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1437,10 +1437,12 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) 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_format_routing_path_network_report_tolerates_malformed_samples(self): From 2d7ad273ef29e01dcb71ac861d9426b2133175da Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:36:59 +0800 Subject: [PATCH 46/63] =?UTF-8?q?feat:=20=E6=91=98=E8=A6=81=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=BA=BF=E6=A7=BD=E6=A1=A5=E6=8E=A5=E6=95=B0=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 2 +- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 17 ++++++++++-- .../freecad_exchange_auto_routing_test.py | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index b65a3de..93acab2 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -87,7 +87,7 @@ terminal_uuid 相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝。 -FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。检查通过时,如果存在相邻线槽自动桥接,报告会显示自动桥接段数,便于确认当前容差是否生效。 +FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。面板摘要和检查报告都会按当前容差显示自动桥接段数,便于确认当前容差是否生效。 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index d6a6b8d..5d8dbcd 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -107,7 +107,16 @@ class AutoRoutingController: payload_wire_count = 0 if isinstance(payload, dict) and isinstance(payload.get("wires"), list): payload_wire_count = len(payload.get("wires") or []) - network = RoutingNetwork.network_summary(doc) + network = RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ) kinds = network.get("kinds", {}) if isinstance(network.get("kinds", {}), dict) else {} kind_text = "" if kinds: @@ -115,7 +124,10 @@ class AutoRoutingController: "{0}={1}".format(key, value) for key, value in sorted(kinds.items()) ) - return "端子:{0};导线任务:{1};QET导线:{2};路由网络:{3} 条 carrier / {4} 段 / {5} 节点{6}".format( + bridge_text = "" + if int(network.get("bridged_segments", 0) or 0) > 0: + bridge_text = ";桥接:{0}".format(network.get("bridged_segments", 0)) + return "端子:{0};导线任务:{1};QET导线:{2};路由网络:{3} 条 carrier / {4} 段 / {5} 节点{6}{7}".format( terminal_count, task_count, payload_wire_count, @@ -123,6 +135,7 @@ class AutoRoutingController: network.get("segments", 0), network.get("nodes", 0), kind_text, + bridge_text, ) def generate_routing_paths(self): diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 8ce07ec..f56c227 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1681,6 +1681,33 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, report["routed"]) self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) + def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(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") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + summary = auto_routing_panel.AutoRoutingController( + options={"adjoining_duct_tolerance": 15.0} + ).summary() + + self.assertIn("桥接:1", summary) + 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() From e689d4edf19f5924bf87c86015cdd5061b355b02 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:45:29 +0800 Subject: [PATCH 47/63] =?UTF-8?q?feat:=20=E5=B8=83=E7=BA=BF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=98=BE=E7=A4=BA=E7=BD=91=E7=BB=9C=E8=B0=83=E5=8F=82?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 +++ src/Mod/FreeCADExchange/AutoRouting.py | 24 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 21 ++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 93acab2..00bebf7 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -370,6 +370,8 @@ QetWiringCutOutBridgeExtensionMm = 20.0 批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。 +批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻线槽自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 + ### 5.3 布线连接功能 已完成: @@ -450,6 +452,7 @@ tests/python/freecad_exchange_auto_routing_test.py 30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 +33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 26fdf9a..f63cf93 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1734,6 +1734,21 @@ def _route_source_sample_text(report): return "" +def _route_network_metric_max(report, key): + maximum = 0 + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + try: + maximum = max(maximum, int(network.get(key, 0) or 0)) + except Exception: + continue + return maximum + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1776,6 +1791,15 @@ def format_eplan_connection_route_report(report): total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) if total_length_mm > 0.0: message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) + bridged_segments = _route_network_metric_max(report, "bridged_segments") + blocked_segments = _route_network_metric_max(report, "blocked_segments") + network_parts = [] + if bridged_segments > 0: + network_parts.append("自动桥接 {0} 段相邻线槽".format(bridged_segments)) + if blocked_segments > 0: + network_parts.append("避障屏蔽 {0} 段".format(blocked_segments)) + if network_parts: + message += "\n路径网络:{0}。".format(",".join(network_parts)) route_source_sample = _route_source_sample_text(report) if route_source_sample: message += "\n{0}".format(route_source_sample) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index f56c227..21a0b2b 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2188,6 +2188,27 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message) + def test_route_report_includes_network_bridge_and_blocked_segment_counts(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": [ + { + "network": { + "bridged_segments": 1, + "blocked_segments": 2, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径网络:自动桥接 1 段相邻线槽,避障屏蔽 2 段。", message) + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From d570c755069566294ab5f33a5b5651e146e48a2e Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:51:02 +0800 Subject: [PATCH 48/63] =?UTF-8?q?feat:=20=E5=B8=83=E7=BA=BF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=98=BE=E7=A4=BA=E5=B9=B6=E8=A1=8C=E9=94=99=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/AutoRouting.py | 34 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 17 ++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 00bebf7..27ab1d1 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -372,6 +372,8 @@ QetWiringCutOutBridgeExtensionMm = 20.0 批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻线槽自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 +如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号和 lane 间距。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。 + ### 5.3 布线连接功能 已完成: @@ -453,6 +455,7 @@ tests/python/freecad_exchange_auto_routing_test.py 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 +34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index f63cf93..41c55af 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1749,6 +1749,34 @@ def _route_network_metric_max(report, key): return maximum +def _route_lane_summary(report): + max_lane_index = 0 + lane_spacing = 0.0 + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + lane = route.get("lane", {}) + if not isinstance(lane, dict): + continue + try: + lane_index = int(lane.get("index", 0) or 0) + except Exception: + lane_index = 0 + if lane_index <= max_lane_index: + continue + max_lane_index = lane_index + try: + lane_spacing = float(lane.get("spacing_mm", 0.0) or 0.0) + except Exception: + lane_spacing = 0.0 + if max_lane_index <= 0: + return {} + return { + "max_lane_index": max_lane_index, + "spacing_mm": lane_spacing, + } + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1800,6 +1828,12 @@ def format_eplan_connection_route_report(report): network_parts.append("避障屏蔽 {0} 段".format(blocked_segments)) if network_parts: message += "\n路径网络:{0}。".format(",".join(network_parts)) + lane_summary = _route_lane_summary(report) + if lane_summary: + message += "\n并行错位:最大 lane {0},间距 {1:.1f} mm。".format( + lane_summary.get("max_lane_index", 0), + float(lane_summary.get("spacing_mm", 0.0) or 0.0), + ) route_source_sample = _route_source_sample_text(report) if route_source_sample: message += "\n{0}".format(route_source_sample) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 21a0b2b..6ca4c19 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2209,6 +2209,23 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("路径网络:自动桥接 1 段相邻线槽,避障屏蔽 2 段。", message) + def test_route_report_includes_parallel_lane_summary(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + {"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "offset_mm": 0.0}}, + {"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "offset_mm": -10.0}}, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("并行错位:最大 lane 2,间距 10.0 mm。", message) + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 1f63e1d72d1dafa9acd1e244c71d603e2abebc4b Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 16:55:43 +0800 Subject: [PATCH 49/63] =?UTF-8?q?feat:=20=E8=AE=B0=E5=BD=95=E5=B8=83?= =?UTF-8?q?=E7=BA=BF=E8=B7=AF=E5=BE=84=E8=BD=BD=E4=BD=93=E5=AE=B9=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++- src/Mod/FreeCADExchange/RoutingNetwork.py | 1 + .../freecad_exchange_auto_routing_test.py | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 27ab1d1..72b55d8 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -366,7 +366,7 @@ QetWiringCutOutBridgeExtensionMm = 20.0 自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier,并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。 -生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象,route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。 +生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象,route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。route track 同时记录 carrier 的 `capacity`,用于后续核对多根线共路、容量偏好和绕行行为。 批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。 @@ -456,6 +456,7 @@ tests/python/freecad_exchange_auto_routing_test.py 32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。 +35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index c7af304..db73c54 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2678,6 +2678,7 @@ def _carrier_track_payload(carrier): "name": getattr(carrier, "Name", ""), "label": getattr(carrier, "Label", ""), "kind": (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND, + "capacity": _route_carrier_capacity_value(carrier, default=1), } source_fields = ( ("source_name", "QetRouteSourceName"), diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 6ca4c19..cc66ed3 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -309,6 +309,27 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("线槽A", wire_duct_carriers[0].get("source_label")) self.assertEqual("WireDuct", wire_duct_carriers[0].get("source_kind")) + def test_route_track_records_carrier_capacity(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + capacity=3, + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + route_track = json.loads(result["wire"].QetRouteTrackJson) + + self.assertEqual(3, route_track["segments"][0]["carrier"]["capacity"]) + def test_network_eplan_connection_route_offsets_lane_by_route_index(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From ae0ecad520fce12b140276b6d1a6a20013302418 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 17:00:42 +0800 Subject: [PATCH 50/63] =?UTF-8?q?feat:=20=E5=B8=83=E7=BA=BF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=8F=90=E7=A4=BA=E5=AE=B9=E9=87=8F=E5=8E=8B=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/AutoRouting.py | 45 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 24 ++++++++++ 3 files changed, 72 insertions(+) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 72b55d8..10e1b22 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -374,6 +374,8 @@ QetWiringCutOutBridgeExtensionMm = 20.0 如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号和 lane 间距。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。 +当最大并行线数超过 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。 + ### 5.3 布线连接功能 已完成: @@ -457,6 +459,7 @@ tests/python/freecad_exchange_auto_routing_test.py 33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。 35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 +36. 批量布线报告会在最大并行线数超过路径最小容量时显示容量提示,但当前仍不做真实填充率计算。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 41c55af..114586e 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1777,6 +1777,45 @@ def _route_lane_summary(report): } +def _route_track_min_capacity(route_track): + if not isinstance(route_track, dict): + return None + capacities = [] + for segment in route_track.get("segments", []) or []: + 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 > 0: + capacities.append(capacity) + if not capacities: + return None + return min(capacities) + + +def _route_capacity_pressure_summary(report, lane_summary): + if not lane_summary: + return {} + max_parallel_wires = int(lane_summary.get("max_lane_index", 0) or 0) + 1 + min_capacity = None + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + route_capacity = _route_track_min_capacity(route.get("route_track", {})) + if route_capacity is None: + continue + min_capacity = route_capacity if min_capacity is None else min(min_capacity, route_capacity) + if min_capacity is None or max_parallel_wires <= min_capacity: + return {} + return { + "max_parallel_wires": max_parallel_wires, + "min_capacity": min_capacity, + } + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1834,6 +1873,12 @@ def format_eplan_connection_route_report(report): lane_summary.get("max_lane_index", 0), float(lane_summary.get("spacing_mm", 0.0) or 0.0), ) + capacity_pressure = _route_capacity_pressure_summary(report, lane_summary) + if capacity_pressure: + message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}。".format( + capacity_pressure.get("max_parallel_wires", 0), + capacity_pressure.get("min_capacity", 0), + ) route_source_sample = _route_source_sample_text(report) if route_source_sample: message += "\n{0}".format(route_source_sample) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index cc66ed3..6a1757b 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2247,6 +2247,30 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("并行错位:最大 lane 2,间距 10.0 mm。", message) + def test_route_report_warns_when_parallel_lanes_exceed_track_capacity(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": [ + { + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "capacity": 2}}, + {"carrier": {"kind": "WireDuct", "capacity": 4}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("容量提示:最大并行线数 3,路径最小容量 2。", message) + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 02bae590179bea6210d4c071d3012aab0cd4fc85 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 17:15:51 +0800 Subject: [PATCH 51/63] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E5=AE=B9?= =?UTF-8?q?=E9=87=8F=E5=8E=8B=E5=8A=9B=E8=AF=AF=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 2 +- src/Mod/FreeCADExchange/AutoRouting.py | 31 ++++++++++--------- .../freecad_exchange_auto_routing_test.py | 31 +++++++++++++++++++ 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 10e1b22..8d6ab5b 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -374,7 +374,7 @@ QetWiringCutOutBridgeExtensionMm = 20.0 如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号和 lane 间距。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。 -当最大并行线数超过 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。 +当单条路线的最大并行线数超过该路线 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。 ### 5.3 布线连接功能 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 114586e..f908e84 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1796,24 +1796,27 @@ def _route_track_min_capacity(route_track): return min(capacities) -def _route_capacity_pressure_summary(report, lane_summary): - if not lane_summary: - return {} - max_parallel_wires = int(lane_summary.get("max_lane_index", 0) or 0) + 1 - min_capacity = None +def _route_capacity_pressure_summary(report): + pressure = {} for route in report.get("routes", []) or []: if not isinstance(route, dict): continue + lane = route.get("lane", {}) + if not isinstance(lane, dict): + continue + try: + max_parallel_wires = int(lane.get("index", 0) or 0) + 1 + except Exception: + max_parallel_wires = 1 route_capacity = _route_track_min_capacity(route.get("route_track", {})) - if route_capacity is None: + if route_capacity is None or max_parallel_wires <= route_capacity: continue - min_capacity = route_capacity if min_capacity is None else min(min_capacity, route_capacity) - if min_capacity is None or max_parallel_wires <= min_capacity: - return {} - return { - "max_parallel_wires": max_parallel_wires, - "min_capacity": min_capacity, - } + if not pressure or max_parallel_wires > pressure.get("max_parallel_wires", 0): + pressure = { + "max_parallel_wires": max_parallel_wires, + "min_capacity": route_capacity, + } + return pressure def format_eplan_connection_route_report(report): @@ -1873,7 +1876,7 @@ def format_eplan_connection_route_report(report): lane_summary.get("max_lane_index", 0), float(lane_summary.get("spacing_mm", 0.0) or 0.0), ) - capacity_pressure = _route_capacity_pressure_summary(report, lane_summary) + capacity_pressure = _route_capacity_pressure_summary(report) if capacity_pressure: message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}。".format( capacity_pressure.get("max_parallel_wires", 0), diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 6a1757b..aacca32 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -2271,6 +2271,37 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("容量提示:最大并行线数 3,路径最小容量 2。", message) + def test_route_report_capacity_pressure_is_checked_per_route(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "capacity": 4}}, + ] + }, + }, + { + "lane": {"index": 0, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "capacity": 1}}, + ] + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertNotIn("容量提示", message) + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 71e2fec765189bf8304cf0790b5d3267d09526bf Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 17:25:43 +0800 Subject: [PATCH 52/63] =?UTF-8?q?feat:=20=E9=9D=A2=E6=9D=BF=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B9=B6=E8=A1=8C=E7=BA=BF=E9=97=B4=E8=B7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 3 ++ src/Mod/FreeCADExchange/AutoRoutingPanel.py | 22 ++++++++++ .../freecad_exchange_auto_routing_test.py | 42 +++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 8d6ab5b..f723d4a 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -91,6 +91,8 @@ FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 +面板还提供“并行线间距 mm”,用于控制多根导线共用同一路径时的可视 lane 偏移距离。它只影响 3D 显示上导线之间的错位间距,不代表真实线槽截面内的排布位置。 + ### 2.1 路由优先级 当前版本按下面优先级处理: @@ -460,6 +462,7 @@ tests/python/freecad_exchange_auto_routing_test.py 34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。 35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 36. 批量布线报告会在最大并行线数超过路径最小容量时显示容量提示,但当前仍不做真实填充率计算。 +37. `3D 布线连接` 面板提供“并行线间距 mm”设置,用于调整多线共路时的可视 lane 偏移距离。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 5d8dbcd..858e5f2 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -99,6 +99,13 @@ class AutoRoutingController: exit_length = AutoRouting.DEFAULT_OPTIONS["terminal_exit_length"] self.options["terminal_exit_length"] = max(exit_length, 0.0) + def set_lane_spacing(self, value): + try: + lane_spacing = float(value) + except Exception: + lane_spacing = AutoRouting.DEFAULT_OPTIONS["lane_spacing"] + self.options["lane_spacing"] = max(lane_spacing, 0.0) + def summary(self): doc = _active_document() terminal_count = len(AutoRouting.index_terminals(doc)) @@ -267,6 +274,20 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.terminal_exit_length_spin) + options_layout.addWidget(QtWidgets.QLabel("并行线间距 mm")) + self.lane_spacing_spin = QtWidgets.QDoubleSpinBox() + self.lane_spacing_spin.setRange(0.0, 1000.0) + self.lane_spacing_spin.setDecimals(1) + self.lane_spacing_spin.setSingleStep(1.0) + self.lane_spacing_spin.setValue( + float( + self.controller.routing_options().get( + "lane_spacing", + AutoRouting.DEFAULT_OPTIONS["lane_spacing"], + ) + ) + ) + options_layout.addWidget(self.lane_spacing_spin) self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间") self.generate_layout_button.setToolTip( @@ -337,6 +358,7 @@ class AutoRoutingTaskPanel: self.controller.set_adjoining_duct_tolerance(self.adjoining_duct_tolerance_spin.value()) self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value()) self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value()) + self.controller.set_lane_spacing(self.lane_spacing_spin.value()) def generate_routing_paths(self): try: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index aacca32..91c27a2 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1729,6 +1729,48 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("桥接:1", summary) + def test_auto_routing_controller_exposes_lane_spacing(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, "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)], + 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", + }, + ], + } + + controller = auto_routing_panel.AutoRoutingController() + controller.set_lane_spacing(14.0) + report = controller.route_eplan_connections() + + 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_rejects_far_network_entry_to_avoid_huge_render_bbox(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From 2302eea33f0b18a6da1884cefb81b0f3da9a32a6 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Mon, 1 Jun 2026 17:33:13 +0800 Subject: [PATCH 53/63] =?UTF-8?q?feat:=20=E9=9D=A2=E6=9D=BF=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=B9=B6=E8=A1=8C=E7=BA=BF=E6=96=B9=E5=90=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 4 +- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 20 +++++++++ .../freecad_exchange_auto_routing_test.py | 43 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index f723d4a..0ca4427 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -91,7 +91,7 @@ FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 -面板还提供“并行线间距 mm”,用于控制多根导线共用同一路径时的可视 lane 偏移距离。它只影响 3D 显示上导线之间的错位间距,不代表真实线槽截面内的排布位置。 +面板还提供“并行线间距 mm”和“并行线方向”,用于控制多根导线共用同一路径时的可视 lane 偏移。方向默认 `auto`,也可以手动指定 `x`、`y`、`z`。这些设置只影响 3D 显示上导线之间的错位方式,不代表真实线槽截面内的排布位置。 ### 2.1 路由优先级 @@ -462,7 +462,7 @@ tests/python/freecad_exchange_auto_routing_test.py 34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。 35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 36. 批量布线报告会在最大并行线数超过路径最小容量时显示容量提示,但当前仍不做真实填充率计算。 -37. `3D 布线连接` 面板提供“并行线间距 mm”设置,用于调整多线共路时的可视 lane 偏移距离。 +37. `3D 布线连接` 面板提供“并行线间距 mm”和“并行线方向”设置,用于调整多线共路时的可视 lane 偏移。 已完成 FreeCAD smoke: diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 858e5f2..97e444f 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -106,6 +106,12 @@ class AutoRoutingController: lane_spacing = AutoRouting.DEFAULT_OPTIONS["lane_spacing"] self.options["lane_spacing"] = max(lane_spacing, 0.0) + def set_lane_axis(self, value): + lane_axis = str(value or "").strip().lower() + if lane_axis not in {"auto", "x", "y", "z"}: + lane_axis = AutoRouting.DEFAULT_OPTIONS["lane_axis"] + self.options["lane_axis"] = lane_axis + def summary(self): doc = _active_document() terminal_count = len(AutoRouting.index_terminals(doc)) @@ -288,6 +294,19 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.lane_spacing_spin) + options_layout.addWidget(QtWidgets.QLabel("并行线方向")) + self.lane_axis_combo = QtWidgets.QComboBox() + self.lane_axis_combo.addItems(["auto", "x", "y", "z"]) + lane_axis = str( + self.controller.routing_options().get( + "lane_axis", + AutoRouting.DEFAULT_OPTIONS["lane_axis"], + ) + or "auto" + ).lower() + axis_index = self.lane_axis_combo.findText(lane_axis) + self.lane_axis_combo.setCurrentIndex(axis_index if axis_index >= 0 else 0) + options_layout.addWidget(self.lane_axis_combo) self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间") self.generate_layout_button.setToolTip( @@ -359,6 +378,7 @@ class AutoRoutingTaskPanel: self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value()) self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value()) self.controller.set_lane_spacing(self.lane_spacing_spin.value()) + self.controller.set_lane_axis(self.lane_axis_combo.currentText()) def generate_routing_paths(self): try: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 91c27a2..923a858 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1771,6 +1771,49 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(14.0, report["routes"][1]["lane"]["spacing_mm"]) self.assertEqual(14.0, report["routes"][1]["lane"]["offset_mm"]) + def test_auto_routing_controller_exposes_lane_axis(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + 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_lane_spacing(8.0) + controller.set_lane_axis("z") + report = controller.route_eplan_connections() + + 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_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() From 5dbd39747bfd60b61521d72b6048e9f59d256e48 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Tue, 2 Jun 2026 16:09:59 +0800 Subject: [PATCH 54/63] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E7=94=B1=E8=B7=AF=E5=BE=84=E5=92=8C=E7=AB=AF=E5=AD=90=E5=B1=80?= =?UTF-8?q?=E9=83=A8=E5=87=BA=E7=BA=BF=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 248 ++++++- src/Mod/FreeCADExchange/AutoRouting.py | 282 ++++++- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 128 +++- src/Mod/FreeCADExchange/RoutingNetwork.py | 237 +++++- src/Mod/FreeCADExchange/TemplateAuthoring.py | 172 +++++ src/Mod/FreeCADExchange/TemplateSemantics.py | 45 ++ src/Mod/FreeCADExchange/TerminalImport.py | 33 +- .../freecad_exchange_auto_routing_test.py | 701 ++++++++++++++++++ ...eecad_exchange_device_import_fcstd_test.py | 6 + ...reecad_exchange_template_authoring_test.py | 77 ++ ...reecad_exchange_template_semantics_test.py | 34 + ...nge_terminal_import_template_slots_test.py | 88 +++ 12 files changed, 1997 insertions(+), 54 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 0ca4427..982d210 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -69,6 +69,175 @@ terminal_uuid 4. 3D 位姿、装配关系、布线几何以 FreeCAD 文档为准。 +### 1.4 金晖隆布线需求提炼 + +根据金晖隆提供的 3D 布线功能需求文档和操作演示视频,当前自动布线需要吸收以下信息。 + +#### 1.4.1 甲方演示中的完整工作流 + +演示流程不是单纯“两个端子之间生成一根线”,而是完整的设备模板、装配、路径和电气关系闭环: + +```text +三维柜体/安装板 + -> 从设备库选择已做好的设备 + -> 设备快速配合到导轨/安装板 + -> 缺失设备按说明书新建三维模块 + -> 为设备定义电气脚点 + -> 为设备定义局部走线路径 + -> 绘制柜内主走线路径 + -> 将设备脚点、局部路径、主路径和 QET 导线关系关联 + -> 生成 3D 布线 +``` + +设备库中的设备不只是几何模型,还应包含: + +1. 外形尺寸。 +2. 电气属性。 +3. 接线脚点。 +4. 设备自身走线路径。 +5. 配合面或安装基准。 + +#### 1.4.2 对当前 FreeCAD 自动布线最有价值的信息 + +1. 柜内布线不应强制依赖线槽。线槽是最高优先级路径,但用户自定义三维草图路径也必须能参与布线。 +2. 设备模板需要保存“局部出线路径”。自动布线不应直接从端子跳到全局主路径,而应支持: + +```text +设备脚点 + -> 设备局部走线路径 + -> 柜内主路径/线槽/用户草图路径 + -> 目标设备局部走线路径 + -> 目标设备脚点 +``` + +3. 端子排、小型断路器等重复设备需要批量插入、自动沿导轨排布、自动配合。 +4. 导轨、线槽、安装板、设备之间的装配关系会直接影响布线路径网络。 +5. 布线完成后需要支持线束高亮和源/目标设备提示。 + +#### 1.4.3 当前阶段暂缓的内容 + +取线机和完整取线表可以放到后面阶段。当前阶段只保留布线结果中必要的基础数据: + +1. 起点端子和终点端子。 +2. 线号/导线标识。 +3. 导线长度。 +4. 导线样式 ID 或 QET 传入的导线规格引用。 +5. 路由路径诊断。 + +导线规格、颜色、线耳等导线数据由 QET 提供,FreeCAD 第一版只消费和保留,不在 3D 侧重新发明导线主数据。 + +### 1.5 设备脚号与 3D 脚点绑定方案 + +设备脚号与 3D 脚点绑定分成两层:模板槽位绑定和工程端子绑定。 + +#### 1.5.1 模板槽位 + +设备模板 `.FCStd` 内,每个可接线脚点使用 LCS 或等价定位对象表示,并保存稳定槽位名: + +```text +QetTemplateSlotName = "A1" | "A2" | "13" | "14" | "P1" | "P2" | ... +QetTerminalLabel = <显示名> +Role = "Terminal" +CanWire = true +``` + +这个槽位名来自设备说明书或设备实物端子标识。它是跨项目复用的模板语义,不保存 `terminal_uuid`。 + +#### 1.5.2 工程端子 + +工程中实例化设备后,FreeCAD 从模板槽位生成工程端子,并补上当前项目和实例信息: + +```text +QetProjectUuid +QetElementUuid +QetInstanceId +QetTerminalUuid +QetTemplateSlotName +``` + +其中 `QetTerminalUuid` 是 2D 端子主键,也是第一版 3D 端子绑定唯一依据。 + +#### 1.5.3 匹配优先级 + +当 QET 导入导线任务或端子数据后,FreeCAD 按下面顺序把 2D 脚号绑定到 3D 脚点: + +1. 如果工程端子已经有相同 `terminal_uuid`,直接使用该工程端子。 +2. 在相同 `instance_id` 的设备下,用 QET 当前已有的端子显示名/脚号,例如 `terminal_display`、`start_terminal_display`、`end_terminal_display`,归一化后匹配 `QetTemplateSlotName` 或 `QetTerminalLabel`。 +3. 如果后续 QET 额外提供 `slot_name_hint`,则可优先用它精确匹配模板槽位;但它不是第一版必需字段。 +4. 如果同一设备下 2D 端子数量和 3D 模板槽位数量一致,允许按顺序兜底匹配,但必须写诊断提示。 +5. 仍无法匹配时,保留为 `local:*` 本地端子,不参与可靠自动布线。 + +#### 1.5.4 QET 需要配合提供的数据 + +第一版数据库仍只使用 `project_2d3d_symbol_binding` 和 `project_2d3d_terminal_binding`。交换 JSON 中当前应优先使用已有的端子显示字段作为匹配提示: + +```json +{ + "terminal_uuid": "2d-terminal-uuid", + "element_uuid": "2d-device-element-uuid", + "instance_id": "3d-device-instance-id", + "terminal_display": "A1" +} +``` + +这些字段只作为 FreeCAD 匹配模板槽位的提示,不写入第一版绑定表,也不能替代 `terminal_uuid`。 + +`slot_name_hint` 只是 FreeCAD 侧预留的可选扩展字段。当前 QET 如果没有该字段,不需要为了第一版专门增加;只要 `terminal_display` / `start_terminal_display` / `end_terminal_display` 能稳定表示设备脚号,就可以完成槽位匹配。 + +QET 侧还需要保证导线任务中继续提供: + +```json +{ + "start_terminal_uuid": "...", + "end_terminal_uuid": "...", + "start_instance_id": "...", + "end_instance_id": "...", + "wire_label": "...", + "wire_style_id": "..." +} +``` + +其中导线规格、颜色、线耳等导线主数据以后由 `wire_style_id` 或等价字段回查 QET。 + +### 1.6 区域与批量排布方案 + +#### 1.6.1 设备区域/柜内区域 + +取线机阶段之前,区域先不写数据库。FreeCAD 文档中可用空间对象或属性对象定义区域: + +```text +QetRegionId +QetRegionName +QetRegionKind = "Cabinet" | "Door" | "MountingPlate" | "LeftBay" | "RightBay" | ... +``` + +设备区域通过 3D 包围盒或设备基准点落在哪个区域对象内推导。后续生成取线表时,再把区域名称写入导出数据。 + +#### 1.6.2 端子排和小型断路器批量排布 + +端子排、小型断路器排布规则参照 SW 的装配思路:导轨是宿主,设备沿导轨轴向按顺序排布。 + +推荐 FreeCAD 文档内保存: + +```text +QetMountHostName = +QetRailAxis = "x" | "y" | "z" +QetRailStartOffsetMm +QetRailPitchMm +QetRailOrderIndex +QetRailDirection = 1 | -1 +``` + +批量插入时: + +1. 用户选择导轨。 +2. 选择设备模板和数量,或从 QET 设备列表生成。 +3. FreeCAD 根据模板宽度、间距、起始偏移和方向计算每个实例的位置。 +4. 每个设备记录宿主导轨和排序号。 +5. 后续导轨移动时,可通过宿主关系刷新设备位置。 + +QET 侧如果能提供端子排/断路器的顺序、数量和显示编号,则 FreeCAD 可以按 2D 顺序生成;如果 QET 暂不提供,第一版由 FreeCAD 用户在 3D 侧手动指定顺序。 + ## 2. 总体方案 布线连接不直接在任意 3D 空间里找线,也不会把所有端子任意两两连接。它参考 EPLAN 的思路,先建立 routing path network,再按 QET 导线任务中的起点端子和终点端子逐条求路。 @@ -246,7 +415,7 @@ edge_cost = segment_length * carrier_kind_factor + bend_penalty WireDuct = 1.0 RoutingPath = 1.0 AuxiliaryPath = 2.0 -RoutingRange = 8.0 +RoutingRange = 25.0 ``` 这使算法优先走线槽和明确路由路径,尽量少走辅助面域。 @@ -492,11 +661,11 @@ tests/manual/freecad_auto_routing_smoke.py ```text 准备布线布局空间:识别并标记 layout space 里的线槽、支撑面、工程端子和障碍处理方式 -生成布线路径网络:按 EPLAN routing path network 逻辑生成 WireDuct、RoutingRange 和 TerminalAccess carrier +生成布线路径网络:按 EPLAN routing path network 逻辑生成 WireDuct、UserPath、RoutingRange 和 TerminalAccess carrier 生成布线连接:先更新布线路径网络,再检查/绑定工程端子,按 QET 导线任务批量求路并生成 AutoSuggested 导线 ``` -如果模型名称/标签足够规范,可以不手动选择,直接执行三步;也可以只点击“生成布线连接”,系统会准备当前可识别的布线路径网络。若线槽无法自动识别,则先选中线槽实体执行“生成布线路径网络”作为补充。 +如果模型名称/标签足够规范,可以不手动选择,直接执行三步;也可以只点击“生成布线连接”,系统会准备当前可识别的布线路径网络。若线槽无法自动识别,则先选中线槽实体执行“生成布线路径网络”作为补充。若甲方现场没有线槽,或需要绕开线槽自由定义柜内主路径,可以选中草图、Draft 线、线段或纯线状对象,再执行“生成布线路径网络”,系统会生成 `UserPath`。 ### 6.2 批量生成布线连接前提 @@ -537,6 +706,75 @@ end_terminal_display 安装板和导轨的机械配合关系会影响对象最终位置,但当前路由器不从装配约束求解位置;它只读取 FreeCAD 文档中已经确定的几何位姿。后续如果增加装配语义,应保存在 FreeCAD 文档中,不扩展第一版数据库绑定表。 +### 6.4 自由路径与设备局部路径 + +为满足甲方“柜内布线不强制依赖线槽,可自由定义任意空间路径”的要求,当前 FreeCAD 侧已支持 `UserPath`: + +1. 选中草图、Draft 线、线段或纯线状对象。 +2. 点击 3D 布线连接面板中的“选中路径作为用户路径”,或直接点击“生成布线路径网络”。 +3. 系统把选中路径转换为 `UserPath` carrier,并参与后续自动布线最短路搜索。“选中路径作为用户路径”只创建用户路径;“生成布线路径网络”会同时更新线槽、布线面、端子接入等完整网络。 +4. 再次选择同一个路径对象生成网络时,系统会刷新原 carrier,不会重复生成。 +5. 如果删除了原草图/线段源对象,再点击“选中路径作为用户路径”或重新生成网络,系统会清理对应的失效 `UserPath` carrier。 + +`UserPath` 与线槽的关系: + +```text +WireDuct:真实线槽,仍是推荐主路径 +UserPath:用户自定义柜内主路径/过渡路径,可在无线槽或特殊路径场景中使用 +RoutingRange:安装板/背板上的低优先级布线面,只作为兜底过渡 +TerminalAccess:端子到主路径的接入段 +``` + +为满足“设备内部/局部走线路径需要连接柜内大路径/主路径”的要求,当前 FreeCAD 侧先提供端子级局部路径字段: + +```text +QetTerminalLocalRoutePointsJson +``` + +字段内容是端子本地坐标系下的点数组,例如: + +```json +[[0, 0, 0], [10, 0, 0], [10, 30, 0]] +``` + +自动生成 `TerminalAccess` 时,系统会先把这些局部点按端子和父设备的 `Placement` 转成全局点,再从局部路径末端连接到最近的柜内主路径、线槽、用户路径或布线面。没有该字段时,仍使用原来的端子 LCS `+Z` 方向短出线。 + +当前模板链路已经支持把局部路径从设备模板带到工程端子。模板 sidecar 或 `QetTemplateSlotsJson` 可以在对应端子槽位上提供: + +```json +{ + "terminal_slots": [ + { + "name": "A1", + "label": "A1", + "base": [10, 20, 30], + "local_route_points": [[0, 0, 0], [10, 0, 0], [10, 30, 0]] + } + ] +} +``` + +导入/更新工程端子时,FreeCAD 会把 `local_route_points` 写入该端子的 `QetTerminalLocalRoutePointsJson`。后续自动生成 `TerminalAccess` 和最终导线几何时都会使用这段局部路径。 + +如果直接在 FCStd 模板端子 LCS 上维护,也可以给模板端子写入同名属性 `QetTerminalLocalRoutePointsJson`。当前模板作者工具提供了内部函数: + +```python +TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) +``` + +其中 `points` 仍是端子本地坐标系下的点数组。导入 FCStd 模板时,FreeCAD 会把 live 模板端子的局部路径保存进设备组的 `QetTemplateSlotsJson`,随后删除 live 模板 LCS,只保留工程端子需要的槽位元数据。 + +模板作者也可以用选择方式维护局部路径: + +```text +1. 在 FCStd 模板文档中先选中一个模板端子 LCS。 +2. 再选中表示局部出线路径的草图、Draft 线或边。 +3. 执行“设置端子局部路径”命令。 +4. 系统把所选路径的文档坐标转换为该端子的本地坐标,并写入 QetTerminalLocalRoutePointsJson。 +``` + +第一版不要求 QET 提供这个字段。它属于 FreeCAD 设备模板/工程端子的 3D 几何元数据,由 FreeCAD 模板作者维护;QET 仍只提供导线任务、设备实例、端子实例和 2D/3D 绑定所需 UUID。 + ## 7. 当前限制 当前版本可完成布线连接原型,但仍有以下限制: @@ -548,7 +786,7 @@ end_terminal_display 5. 未做强弱电分槽、线缆类型隔离。 6. 障碍检测基于 AABB,存在误报和漏报。 7. 辅助路由区域是网格近似,不等于专业软件的完整布线区域建模。 -8. 端子出线方向依赖端子 LCS 方向;如果模板端子方向不准,布线连接会受影响。 +8. 端子出线方向默认依赖端子 LCS 方向;若工程端子提供 `QetTerminalLocalRoutePointsJson`,会优先使用局部路径。模板端子方向或局部路径不准时,布线连接会受影响。 9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。 10. 当前不自动求解导轨、安装板和设备之间的 Assembly 配合关系;装配位置以 `scene.FCStd` 中对象的最终 `Placement` 为准。 @@ -586,7 +824,7 @@ WireDuct: 4. 端子出线规则 ```text -根据端子 LCS 方向、设备安装面、接线孔方向生成更合理的短出线段 +根据端子 LCS 方向、设备安装面、接线孔方向和模板局部路径生成更合理的短出线段 ``` ### 8.2 中期能力 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index f908e84..e697d2c 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -40,7 +40,7 @@ DEFAULT_OPTIONS = { "UserPath": 1.0, "AuxiliaryPath": 2.0, "TerminalAccess": 2.0, - "RoutingRange": 8.0, + "RoutingRange": 25.0, }, # 主干必须走 carrier/贴面网络;没有布线路径网络时直接失败。 # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 @@ -54,6 +54,7 @@ DEFAULT_OPTIONS = { # 替代路径,再退回原图并用 CollisionWarning 告诉用户当前网络不足。 "avoid_obstacles": True, "replace_existing": True, + "hide_route_carriers_after_route": True, } @@ -563,6 +564,7 @@ def _bind_wire_task_terminals(doc, payload): label=terminal_display or getattr(terminal_obj, "Label", "") or terminal_uuid, slot_name=slot_name, ) + TerminalObjects.hide_engineering_terminal(terminal_obj) used_objects.add(terminal_obj) slot_token = _normalized_match_token(slot_name) if slot_token: @@ -766,10 +768,12 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non return None exit_length = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) - start_origin = _terminal_origin(start_terminal) - end_origin = _terminal_origin(end_terminal) - start_exit = _offset(start_origin, _terminal_direction(start_terminal), exit_length) - end_exit = _offset(end_origin, _terminal_direction(end_terminal), exit_length) + start_access_points = RoutingNetwork.terminal_access_path_points(start_terminal, exit_length) + end_access_points = RoutingNetwork.terminal_access_path_points(end_terminal, exit_length) + start_origin = start_access_points[0] if start_access_points else _terminal_origin(start_terminal) + end_origin = end_access_points[0] if end_access_points else _terminal_origin(end_terminal) + start_exit = start_access_points[-1] if start_access_points else _offset(start_origin, _terminal_direction(start_terminal), exit_length) + end_exit = end_access_points[-1] if end_access_points else _offset(end_origin, _terminal_direction(end_terminal), exit_length) def route_on_network(network, obstacle_aware=False): if network.get("segment_count", 0) <= 0: @@ -807,13 +811,14 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non carrier_points = _apply_lane_offset(carrier_points, lane) points = [] - _append_unique(points, start_origin) - _append_unique(points, start_exit) + for point in start_access_points or [start_origin, start_exit]: + _append_unique(points, point) _append_orthogonal(points, carrier_points[0]) for point in carrier_points[1:]: _append_unique(points, point) _append_orthogonal(points, end_exit) - _append_unique(points, end_origin) + for point in reversed(end_access_points or [end_origin, end_exit]): + _append_unique(points, point) points = _simplify_collinear_points( points, preserved_point_keys=_important_route_node_keys(network, path_keys, path_result), @@ -1185,6 +1190,7 @@ def route_eplan_connection_between_terminals( wire_mark_is_manual=False, wire_style_id="", endpoint_metadata=None, + defer_recompute=False, ): if doc is None: raise AutoRoutingError("No FreeCAD document is available.") @@ -1237,6 +1243,7 @@ def route_eplan_connection_between_terminals( wire_name = _unique_name(doc, _wire_object_name(start_terminal, end_terminal, wire_uuid)) wire = None + removed_existing = 0 try: wire = _create_wire_geometry(doc, wire_name, points) wire.Label = wire_label or wire_mark or wire_uuid or "QET Routed Connection" @@ -1288,10 +1295,11 @@ def route_eplan_connection_between_terminals( _remove_routing_connection_objects(doc, [wire]) raise AutoRoutingError("Failed to replace existing routed connection.") - try: - doc.recompute() - except Exception: - pass + if not defer_recompute: + try: + doc.recompute() + except Exception: + pass return { "wire": wire, @@ -1304,6 +1312,7 @@ def route_eplan_connection_between_terminals( "length_mm": _route_length(points), "collision_count": len(collisions), "collisions": collisions, + "replaced_routed_connections": removed_existing, } @@ -1436,6 +1445,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la if not isinstance(payload, dict): raise AutoRoutingError("Exchange payload must be an object.") + opts = _merged_options(options) terminal_binding_report = bind_wire_task_terminals_from_payload(doc, payload) terminals = index_terminals(doc) local_terminal_count = sum( @@ -1453,11 +1463,14 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "auto_terminal_binding_warnings": terminal_binding_report["warnings"], "routed": 0, "collision_warnings": 0, + "replaced_routed_connections": 0, "total_length_mm": 0.0, "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, "skipped_invalid": 0, "missing_endpoint_uuids": [], "missing_endpoint_samples": [], + "missing_route_network_samples": [], "collision_samples": [], "errors": [], "error_samples": [], @@ -1466,6 +1479,18 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } if isinstance(prepared_layout, dict): report["prepared_layout"] = prepared_layout + try: + route_network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + except Exception as exc: + route_network = {} + report["route_network_error"] = str(exc) + report["route_network_carriers"] = int(route_network.get("carrier_count", 0) or 0) + report["route_network_segments"] = int(route_network.get("segment_count", 0) or 0) + report["route_network_nodes"] = len(route_network.get("nodes", {}) or {}) + has_route_network = report["route_network_segments"] > 0 missing_endpoint_uuids = set() lane_indexes_by_pair = {} lane_indexes_by_segment = {} @@ -1478,6 +1503,12 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la key = str(status or "").strip() or "Unknown" report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 + def set_item_task_status(item, status): + wire_uuid = _wire_item_value(item, "wire_id", "wire_uuid", "id") + if not wire_uuid: + return + _set_task_status(_find_task_by_wire_uuid(doc, wire_uuid), status) + def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): route_options = dict(options or {}) if isinstance(item, dict) and "__segment_usage_costs" in item: @@ -1496,6 +1527,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la wire_mark_is_manual=bool(item.get("wire_mark_is_manual", False)), wire_style_id=_wire_item_value(item, "wire_style_id"), endpoint_metadata=endpoint_metadata, + defer_recompute=True, ) for item in wires: @@ -1510,6 +1542,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la 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: missing_endpoint_uuids.add(terminal_uuid) @@ -1530,6 +1563,27 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } ) continue + if not has_route_network: + report["skipped_missing_route_network"] += 1 + add_status("MissingRouteNetwork") + set_item_task_status(item, "MissingRouteNetwork") + if len(report["missing_route_network_samples"]) < 8: + report["missing_route_network_samples"].append( + { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label"), + "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + } + ) + continue lane_key = _route_lane_key(start_uuid, end_uuid) route_lane_index = lane_indexes_by_pair.get(lane_key, 0) try: @@ -1573,6 +1627,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la error_text = str(exc) report["errors"].append(error_text) add_status("Error") + set_item_task_status(item, "Error") if len(report["error_samples"]) < 8: report["error_samples"].append( { @@ -1603,6 +1658,9 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la segment_usage_costs[segment_key] = segment_usage_costs.get(segment_key, 0) + 1 if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 + report["replaced_routed_connections"] += int( + result.get("replaced_routed_connections", 0) or 0 + ) add_status(result["route_status"]) route_collision_samples = [] for collision in list(result.get("collisions", []) or [])[:3]: @@ -1645,6 +1703,11 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "collision_samples": route_collision_samples, } ) + if report["routed"] > 0: + try: + doc.recompute() + except Exception: + pass report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) _write_routing_connection_batch_diagnostic(doc, report) return report @@ -1819,6 +1882,61 @@ def _route_capacity_pressure_summary(report): return pressure +_ROUTE_QUALITY_WARNING_KIND_LABELS = { + "RoutingRange": "布线面", + "AuxiliaryPath": "辅助路径", +} + + +def _route_track_carrier_kinds(route_track): + if not isinstance(route_track, dict): + return {} + carrier_kinds = route_track.get("carrier_kinds", {}) + if isinstance(carrier_kinds, dict) and carrier_kinds: + return { + str(key): value + for key, value in carrier_kinds.items() + if str(key).strip() + } + counts = {} + for segment in route_track.get("segments", []) or []: + carrier = segment.get("carrier", {}) if isinstance(segment, dict) else {} + if not isinstance(carrier, dict): + continue + kind = str(carrier.get("kind", "") or "").strip() + if kind: + counts[kind] = counts.get(kind, 0) + 1 + return counts + + +def _route_quality_warning_summary(report): + warning_count = 0 + sample = None + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + carrier_kinds = _route_track_carrier_kinds(route.get("route_track", {})) + warning_labels = [ + label + for kind, label in _ROUTE_QUALITY_WARNING_KIND_LABELS.items() + if carrier_kinds.get(kind, 0) + ] + if not warning_labels: + continue + warning_count += 1 + if sample is None: + sample = { + "wire": _wire_sample_text(route), + "labels": warning_labels, + } + if warning_count <= 0: + return {} + return { + "count": warning_count, + "sample": sample or {}, + } + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1832,6 +1950,7 @@ def format_eplan_connection_route_report(report): "CollisionWarning": "碰撞告警", "Error": "错误", "MissingTerminal": "缺失端子", + "MissingRouteNetwork": "缺少布线路径网络", "Invalid": "无效任务", } def status_count_value(value): @@ -1840,7 +1959,14 @@ def format_eplan_connection_route_report(report): except Exception: return 0 status_parts = [] - for key in ("Routed", "CollisionWarning", "Error", "MissingTerminal", "Invalid"): + for key in ( + "Routed", + "CollisionWarning", + "Error", + "MissingTerminal", + "MissingRouteNetwork", + "Invalid", + ): value = status_count_value(status_counts.get(key, 0)) if value > 0: status_parts.append("{0} {1} 条".format(status_labels[key], value)) @@ -1858,9 +1984,19 @@ def format_eplan_connection_route_report(report): prepared_layout.get("surface_carriers", 0), prepared_layout.get("terminal_access_carriers", 0), ) + if report.get("skipped_missing_route_network", 0) > 0: + message += "\n缺少布线路径网络:{0} 条导线已跳过。请先生成线槽、布线面或布线路径网络。".format( + report.get("skipped_missing_route_network", 0) + ) total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) if total_length_mm > 0.0: message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) + replaced_routed_connections = int(report.get("replaced_routed_connections", 0) or 0) + if replaced_routed_connections > 0: + message += "\n已替换旧布线连接:{0} 条。".format(replaced_routed_connections) + hidden_route_carriers = int(report.get("hidden_route_carriers", 0) or 0) + if hidden_route_carriers > 0: + message += "\n已隐藏走线路径辅助对象:{0} 条。".format(hidden_route_carriers) bridged_segments = _route_network_metric_max(report, "bridged_segments") blocked_segments = _route_network_metric_max(report, "blocked_segments") network_parts = [] @@ -1885,6 +2021,17 @@ def format_eplan_connection_route_report(report): route_source_sample = _route_source_sample_text(report) if route_source_sample: message += "\n{0}".format(route_source_sample) + quality_warning = _route_quality_warning_summary(report) + if quality_warning: + message += "\n路径质量提示:{0} 条导线使用布线面/辅助路径,可能没有完全优先进入线槽。".format( + quality_warning.get("count", 0) + ) + sample = quality_warning.get("sample", {}) + if isinstance(sample, dict) and sample.get("wire") and sample.get("labels"): + message += " 示例 {0} 使用{1}。".format( + sample.get("wire"), + "、".join(sample.get("labels", []) or []), + ) errors = report.get("errors", []) or [] if errors: message += "\n首个错误:{0}".format(str(errors[0])) @@ -1970,6 +2117,95 @@ def _clear_routing_connection_batch_diagnostics(doc): return removed +def _compact_route_sample(route): + route_track = route.get("route_track", {}) if isinstance(route, dict) else {} + if not isinstance(route_track, dict): + route_track = {} + sample = { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "start_element_uuid": route.get("start_element_uuid", ""), + "start_terminal_display": route.get("start_terminal_display", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "end_element_uuid": route.get("end_element_uuid", ""), + "end_terminal_display": route.get("end_terminal_display", ""), + "endpoint_label": route.get("endpoint_label", ""), + "route_status": route.get("route_status", ""), + "length_mm": route.get("length_mm", 0.0), + "lane": route.get("lane", {}), + "algorithm": route.get("algorithm", ""), + "collision_count": route.get("collision_count", 0), + "carrier_kinds": route_track.get("carrier_kinds", {}), + "carrier_names": list(route_track.get("carrier_names", []) or [])[:8], + "route_source_labels": _route_source_labels(route_track, limit=8), + } + network = route.get("network", {}) + if isinstance(network, dict): + sample["network"] = { + "carriers": network.get("carriers", 0), + "segments": network.get("segments", 0), + "bridged_segments": network.get("bridged_segments", 0), + "blocked_segments": network.get("blocked_segments", 0), + "entry_distance": network.get("entry_distance", 0.0), + "exit_distance": network.get("exit_distance", 0.0), + } + return sample + + +def _compact_routing_connection_batch_report(report, sample_limit=8): + if not isinstance(report, dict): + return {} + limit = max(int(sample_limit or 0), 0) + payload = {} + scalar_keys = ( + "total_wires", + "available_terminals", + "local_terminals", + "auto_bound_terminals", + "auto_created_terminals", + "routed", + "collision_warnings", + "replaced_routed_connections", + "total_length_mm", + "skipped_missing_terminal", + "skipped_missing_route_network", + "skipped_invalid", + "route_network_carriers", + "route_network_segments", + "route_network_nodes", + "route_network_error", + "hidden_route_carriers", + ) + for key in scalar_keys: + if key in report: + payload[key] = report.get(key) + if isinstance(report.get("prepared_layout"), dict): + payload["prepared_layout"] = report.get("prepared_layout") + if isinstance(report.get("route_status_counts"), dict): + payload["route_status_counts"] = dict(report.get("route_status_counts") or {}) + missing_endpoint_uuids = list(report.get("missing_endpoint_uuids", []) or []) + payload["missing_endpoint_uuid_count"] = len(missing_endpoint_uuids) + payload["missing_endpoint_uuids"] = missing_endpoint_uuids[:50] + for key in ( + "auto_terminal_binding_warnings", + "missing_endpoint_samples", + "missing_route_network_samples", + "collision_samples", + "error_samples", + "errors", + ): + values = list(report.get(key, []) or []) + payload[key] = values[:limit] + payload["{0}_count".format(key)] = len(values) + routes = [route for route in list(report.get("routes", []) or []) if isinstance(route, dict)] + payload["route_count"] = len(routes) + payload["route_samples"] = [_compact_route_sample(route) for route in routes[:limit]] + payload["route_sample_count"] = len(payload["route_samples"]) + payload["diagnostic_payload"] = "compact-routing-connection-batch-v1" + return payload + + def _write_routing_connection_batch_diagnostic(doc, report): if doc is None or not isinstance(report, dict): return None @@ -1990,7 +2226,7 @@ def _write_routing_connection_batch_diagnostic(doc, report): _set_string( diagnostic, "QetDiagnosticJson", - json.dumps(report, ensure_ascii=False), + json.dumps(_compact_routing_connection_batch_report(report), ensure_ascii=False), "QET routing connection batch diagnostic payload", ) group.addObject(diagnostic) @@ -2229,13 +2465,14 @@ def route_eplan_connections( """Route QET wire tasks through the EPLAN-style routing path network.""" if doc is None: raise AutoRoutingError("No FreeCAD document is available.") + opts = _merged_options(options) prepared_network = None if update_network: prepared_network = update_eplan_routing_path_network( doc, project_uuid=project_uuid, - options=options, + options=opts, selection_ex=selection_ex, ) @@ -2247,13 +2484,13 @@ def route_eplan_connections( report = route_eplan_connections_from_payload( doc, target_payload, - options=options, + options=opts, prepared_layout=prepared_network, ) else: report = route_eplan_connection_tasks( doc, - options=options, + options=opts, prepared_layout=prepared_network, ) @@ -2261,6 +2498,10 @@ def route_eplan_connections( report["routing_path_network_updated"] = bool(update_network) if isinstance(prepared_network, dict): report["routing_path_network"] = prepared_network + if opts.get("hide_route_carriers_after_route", True): + report["hidden_route_carriers"] = RoutingNetwork.set_route_carriers_visibility(doc, False) + else: + report["hidden_route_carriers"] = 0 return report def wire_task_count(doc): @@ -2269,15 +2510,22 @@ def wire_task_count(doc): def clear_routing_connections(doc): removed = 0 + removed_wire_uuids = set() for obj in list(WiringObjects.iter_routed_wire_objects(doc)): if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": continue + wire_uuid = (getattr(obj, "QetWireUuid", "") or "").strip() try: _detach_object_from_groups(doc, obj) doc.removeObject(obj.Name) removed += 1 + if wire_uuid: + removed_wire_uuids.add(wire_uuid) except Exception: pass + for wire_uuid in sorted(removed_wire_uuids): + _set_task_status(_find_task_by_wire_uuid(doc, wire_uuid), "Task") + _clear_routing_connection_batch_diagnostics(doc) try: doc.recompute() except Exception: diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 97e444f..f3c766c 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -70,6 +70,32 @@ def _selection_ex(): return [] +def _style_command_button(button, text, tooltip=""): + """Keep task-panel command button text visible across FreeCAD themes.""" + if button is None: + return button + try: + button.setText(text) + except Exception: + pass + try: + button.setToolTip(tooltip) + except Exception: + pass + try: + button.setMinimumHeight(28) + except Exception: + pass + try: + button.setStyleSheet( + "QPushButton { color: #202020; text-align: left; padding-left: 8px; }" + "QPushButton:disabled { color: #606060; }" + ) + except Exception: + pass + return button + + class AutoRoutingController: def __init__(self, options=None): self.last_report = None @@ -187,6 +213,31 @@ class AutoRoutingController: self.last_report["source_mode"] = "document" return self.last_report + def create_user_paths_from_selection(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + removed_stale_carriers = RoutingNetwork.cleanup_invalid_source_carriers(doc) + created = RoutingNetwork.create_user_path_carriers_from_selection( + doc, + _selection_ex(), + project_uuid=project_uuid, + ) + self.last_report = { + "user_path_carriers": len(created), + "removed_stale_carriers": removed_stale_carriers, + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + def route_eplan_connections(self): doc = _active_document() project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() @@ -308,35 +359,46 @@ class AutoRoutingTaskPanel: self.lane_axis_combo.setCurrentIndex(axis_index if axis_index >= 0 else 0) options_layout.addWidget(self.lane_axis_combo) - self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间") - self.generate_layout_button.setToolTip( - "按 EPLAN 布局空间语义识别线槽、安装面、工程端子和障碍处理方式,不生成导线。" + self.generate_layout_button = _style_command_button( + QtWidgets.QPushButton(), + "准备布线布局空间", + "按 EPLAN 布局空间语义识别线槽、安装面、工程端子和障碍处理方式,不生成导线。", + ) + + self.generate_paths_button = _style_command_button( + QtWidgets.QPushButton(), + "生成布线路径网络", + "按 EPLAN 逻辑从布局空间生成完整 routing path network:线槽、布线区域和端子接入。", ) - self.generate_paths_button = QtWidgets.QPushButton("生成布线路径网络") - self.generate_paths_button.setToolTip( - "按 EPLAN 逻辑从布局空间生成完整 routing path network:线槽、布线区域和端子接入。" + self.create_user_paths_button = _style_command_button( + QtWidgets.QPushButton(), + "选中路径作为用户路径", + "把选中的草图、线段或纯线状对象转换为可参与自动布线的 UserPath。", ) - self.check_paths_button = QtWidgets.QPushButton("检查布线路径网络") - self.check_paths_button.setToolTip( - "检查 routing path network 的断点、孤立网络和未接入端子,并写入诊断对象。" + self.check_paths_button = _style_command_button( + QtWidgets.QPushButton(), + "检查布线路径网络", + "检查 routing path network 的断点、孤立网络和未接入端子,并写入诊断对象。", ) - self.route_connections_button = QtWidgets.QPushButton("生成布线连接") - self.route_connections_button.setToolTip( - "自动更新布线路径网络,并按全部 QET 导线任务生成 3D 布线连接。" + self.route_connections_button = _style_command_button( + QtWidgets.QPushButton(), + "生成布线连接", + "自动更新布线路径网络,并按全部 QET 导线任务生成 3D 布线连接。", ) - self.clear_routes_button = QtWidgets.QPushButton("清除布线连接") - self.clear_carriers_button = QtWidgets.QPushButton("清除走线路径") - self.save_button = QtWidgets.QPushButton("保存") + self.clear_routes_button = _style_command_button(QtWidgets.QPushButton(), "清除布线连接") + self.clear_carriers_button = _style_command_button(QtWidgets.QPushButton(), "清除走线路径") + self.save_button = _style_command_button(QtWidgets.QPushButton(), "保存") self.status_label = QtWidgets.QLabel("") self.status_label.setWordWrap(True) for widget in ( self.generate_layout_button, + self.create_user_paths_button, self.generate_paths_button, self.check_paths_button, self.route_connections_button, @@ -350,6 +412,7 @@ class AutoRoutingTaskPanel: layout.addWidget(self.status_label) self.generate_paths_button.clicked.connect(self.generate_routing_paths) + self.create_user_paths_button.clicked.connect(self.create_user_paths_from_selection) self.check_paths_button.clicked.connect(self.check_routing_path_network) self.generate_layout_button.clicked.connect(self.generate_layout_space) self.route_connections_button.clicked.connect(self.route_eplan_connections) @@ -385,18 +448,20 @@ class AutoRoutingTaskPanel: self._sync_options_from_widgets() result = self.controller.generate_routing_paths() wire_ducts = result.get("wire_duct_carriers", 0) + user_paths = result.get("user_path_carriers", 0) surfaces = result.get("surface_carriers", 0) terminal_access = result.get("terminal_access_carriers", 0) network = result.get("network", {}) if isinstance(result.get("network", {}), dict) else {} if network.get("segments", 0) == 0: self._set_status( - "未生成可用布线路径网络。可选中线槽实体,或确认安装板/背板能作为布线区域。" + "未生成可用布线路径网络。可选中线槽实体、草图/线段作为用户路径,或确认安装板/背板能作为布线区域。" + self.controller.summary() ) return self._set_status( - "已生成布线路径网络:线槽路径 {0} 条,布线区域 {1} 条,端子接入 {2} 条,网络段 {3} 条。{4}".format( + "已生成布线路径网络:线槽路径 {0} 条,用户路径 {1} 条,布线区域 {2} 条,端子接入 {3} 条,网络段 {4} 条。{5}".format( wire_ducts, + user_paths, surfaces, terminal_access, network.get("segments", 0), @@ -406,6 +471,35 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def create_user_paths_from_selection(self): + try: + self._sync_options_from_widgets() + result = self.controller.create_user_paths_from_selection() + created = result.get("user_path_carriers", 0) + removed = result.get("removed_stale_carriers", 0) + if created <= 0: + stale_text = "" + if removed > 0: + stale_text = "已清理失效用户路径:{0} 条。".format(removed) + self._set_status( + stale_text + + "未创建用户路径。请先选择草图、Draft 线、线段或纯线状对象。" + + self.controller.summary() + ) + return + stale_text = "" + if removed > 0: + stale_text = "已清理失效用户路径:{0} 条。".format(removed) + self._set_status( + "{0}已创建用户路径:{1} 条。{2}".format( + stale_text, + created, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + def check_routing_path_network(self): try: self._sync_options_from_widgets() diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index db73c54..fefcf2e 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -19,6 +19,7 @@ ROUTE_CARRIER_KIND = "RoutingPath" ROUTE_CARRIER_KIND_WIRE_DUCT = "WireDuct" ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END = "WireDuctOpenEnd" ROUTE_CARRIER_KIND_WIRING_CUT_OUT = "WiringCutOut" +ROUTE_CARRIER_KIND_USER_PATH = "UserPath" ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath" ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange" ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess" @@ -26,6 +27,7 @@ MANAGED_ROUTE_SOURCE_KINDS = { ROUTE_CARRIER_KIND_WIRE_DUCT, ROUTE_CARRIER_KIND_WIRING_CUT_OUT, ROUTE_CARRIER_KIND_ROUTING_RANGE, + ROUTE_CARRIER_KIND_USER_PATH, } PROPERTY_GROUP = "QET Routing" DEFAULT_NODE_TOLERANCE = 0.001 @@ -114,8 +116,8 @@ DEFAULT_KIND_COST_FACTORS = { ROUTE_CARRIER_KIND: 1.0, ROUTE_CARRIER_KIND_AUXILIARY_PATH: 2.0, ROUTE_CARRIER_KIND_TERMINAL_ACCESS: 2.0, - ROUTE_CARRIER_KIND_ROUTING_RANGE: 8.0, - "UserPath": 1.0, + ROUTE_CARRIER_KIND_ROUTING_RANGE: 25.0, + ROUTE_CARRIER_KIND_USER_PATH: 1.0, } ROUTE_CARRIER_VIEW_STYLES = { ROUTE_CARRIER_KIND_WIRE_DUCT: { @@ -142,6 +144,10 @@ ROUTE_CARRIER_VIEW_STYLES = { "color": (0.45, 0.45, 0.45), "width": 2.0, }, + ROUTE_CARRIER_KIND_USER_PATH: { + "color": (0.95, 0.15, 0.15), + "width": 3.0, + }, ROUTE_CARRIER_KIND: { "color": (0.0, 0.45, 0.85), "width": 2.0, @@ -587,6 +593,18 @@ def _set_wiring_cut_out_source_semantics(source, bridge_extension=DEFAULT_WIRING ) +def _set_user_path_source_semantics(source): + if source is None: + return + TerminalObjects.ensure_string_property( + source, + "QetRoutingSourceKind", + PROPERTY_GROUP, + "Routing source kind", + ROUTE_CARRIER_KIND_USER_PATH, + ) + + def _style_route_carrier(carrier, kind): style = ROUTE_CARRIER_VIEW_STYLES.get(kind) or ROUTE_CARRIER_VIEW_STYLES[ROUTE_CARRIER_KIND] try: @@ -707,8 +725,11 @@ def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARR def is_route_carrier(obj): if obj is None: return False - role = (getattr(obj, "QetRoutingRole", "") or "").strip() - return role == ROUTING_ROLE and bool(getattr(obj, "CanRouteWire", False)) + try: + role = (getattr(obj, "QetRoutingRole", "") or "").strip() + return role == ROUTING_ROLE and bool(getattr(obj, "CanRouteWire", False)) + except Exception: + return False def _carrier_points(obj): @@ -892,6 +913,22 @@ def collect_route_carriers(doc): return result +def set_route_carriers_visibility(doc, visible): + """Show or hide generated route carrier helper objects.""" + updated = 0 + for carrier in collect_route_carriers(doc): + try: + carrier.ViewObject.Visibility = bool(visible) + updated += 1 + except Exception: + pass + try: + doc.recompute() + except Exception: + pass + return updated + + def _detach_from_groups(doc, obj): for parent in list(getattr(obj, "InList", []) or []): group = list(getattr(parent, "Group", []) or []) @@ -1418,6 +1455,61 @@ def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUT return created +def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid=""): + """Create or refresh user-defined spatial route paths from selected sketches/edges.""" + cleanup_invalid_source_carriers(doc) + created = [] + support_face = _support_face_from_selection(selection_ex) + seen_sources = set() + for index, item in enumerate(selection_ex or [], start=1): + if support_face is not None and _selection_item_is_only_support_face(item): + continue + source = getattr(item, "Object", None) + if source is not None: + if id(source) in seen_sources: + continue + seen_sources.add(id(source)) + if ( + _is_wire_duct_candidate(source) + or _is_support_surface_candidate(source) + or _is_wiring_cut_out_candidate(source) + ): + continue + points = _points_from_selection_item(item) + if len(points) < 2: + continue + if support_face is not None: + points = _project_points_to_face(points, support_face) + + label = "QET User Route Path {0}".format(index) + if source is not None: + label = "QET User Route Path {0}".format( + getattr(source, "Label", "") or getattr(source, "Name", "") or index + ) + live_carrier = _live_source_carrier(doc, source) + if live_carrier is not None: + if _update_route_carrier( + live_carrier, + points, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + ): + _mark_user_path_source(source, live_carrier) + continue + + carrier = create_route_carrier( + doc, + points, + label=label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + ) + if source is not None: + _mark_user_path_source(source, carrier) + created.append(carrier) + return created + + def _wire_duct_centerline_spec_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): extents = { axis: _bbox_extent(bbox, axis) @@ -1702,6 +1794,23 @@ def _mark_wiring_cut_out_source(source, carrier, bridge_extension=DEFAULT_WIRING pass +def _mark_user_path_source(source, carrier): + if source is None or carrier is None: + return + try: + _set_user_path_source_semantics(source) + TerminalObjects.ensure_string_property( + source, + "QetRouteCarrierName", + PROPERTY_GROUP, + "Generated route carrier for this source", + getattr(carrier, "Name", ""), + ) + _remember_source_carriers(source, [carrier]) + except Exception: + pass + + def _mark_terminal_access_source(source, carrier): if source is None or carrier is None: return @@ -1737,6 +1846,8 @@ def _source_is_valid_for_kind(source, source_kind): return _is_support_surface_candidate(source) if source_kind == ROUTE_CARRIER_KIND_WIRING_CUT_OUT: return _is_wiring_cut_out_candidate(source) + if source_kind == ROUTE_CARRIER_KIND_USER_PATH: + return _is_route_path_source_object(source) return True @@ -2103,6 +2214,101 @@ def _terminal_exit_point(terminal, exit_length): return _add(origin, _scale(direction, max(float(exit_length or 0.0), 0.0))) +def _json_route_point(item): + try: + if isinstance(item, dict): + return App.Vector( + float(item.get("x", 0.0) or 0.0), + float(item.get("y", 0.0) or 0.0), + float(item.get("z", 0.0) or 0.0), + ) + if isinstance(item, (list, tuple)) and len(item) >= 3: + return App.Vector(float(item[0] or 0.0), float(item[1] or 0.0), float(item[2] or 0.0)) + except Exception: + return None + return None + + +def _terminal_local_route_points(terminal): + for property_name in ("QetTerminalLocalRoutePointsJson", "QetLocalRoutePointsJson"): + raw = (getattr(terminal, property_name, "") or "").strip() + if not raw: + continue + try: + parsed = json.loads(raw) + except Exception: + continue + points = [_json_route_point(item) for item in parsed if item is not None] + points = [point for point in points if point is not None] + if points: + return points + return [] + + +def _terminal_parent_chain(terminal): + chain = [] + current = terminal + visited = set() + while current is not None: + parents = list(getattr(current, "InList", []) or []) + parent = None + for candidate in parents: + if id(candidate) in visited: + continue + if getattr(candidate, "Placement", None) is not None: + parent = candidate + break + if parent is None: + break + visited.add(id(parent)) + chain.append(parent) + current = parent + return chain + + +def _placement_mult_vec(placement, point): + if placement is None: + return point + try: + transformed = placement.multVec(point) + if transformed is not None: + return _vector(transformed) + except Exception: + pass + base = getattr(placement, "Base", None) + if base is not None: + return App.Vector(point.x + base.x, point.y + base.y, point.z + base.z) + return point + + +def _terminal_local_point_to_global(terminal, local_point): + try: + if hasattr(terminal, "getGlobalPlacement"): + placement = terminal.getGlobalPlacement() + return _placement_mult_vec(placement, _vector(local_point)) + except Exception: + pass + + point = _placement_mult_vec(getattr(terminal, "Placement", None), _vector(local_point)) + for parent in _terminal_parent_chain(terminal): + point = _placement_mult_vec(getattr(parent, "Placement", None), point) + return point + + +def terminal_access_path_points(terminal, exit_length=20.0): + """Return terminal-to-network access points, honoring optional local route metadata.""" + origin = _vector(TerminalObjects.terminal_origin(terminal)) + local_points = _terminal_local_route_points(terminal) + if local_points: + 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) + normalized = _normalized_route_points(points) + if len(normalized) >= 2: + return normalized + return _normalized_route_points([origin, _terminal_exit_point(terminal, exit_length)]) + + def _orthogonal_access_points(start, end): """Create a Manhattan path so access carriers can join the routing graph.""" start = _vector(start) @@ -2162,7 +2368,11 @@ def create_terminal_access_carriers_from_document( for terminal in _collect_routable_terminals(doc): if _live_source_carrier(doc, terminal) is not None: continue - exit_point = _terminal_exit_point(terminal, terminal_exit_length) + has_local_route_points = bool(_terminal_local_route_points(terminal)) + terminal_access_points = terminal_access_path_points(terminal, terminal_exit_length) + if len(terminal_access_points) < 2: + continue + exit_point = terminal_access_points[-1] nearest_point, distance = nearest_point_on_network(network, exit_point) if nearest_point is None: continue @@ -2171,7 +2381,13 @@ def create_terminal_access_carriers_from_document( if float(distance or 0.0) <= DEFAULT_NODE_TOLERANCE: continue - points = _orthogonal_access_points(exit_point, nearest_point) + if has_local_route_points: + points = list(terminal_access_points) + for point in _orthogonal_access_points(exit_point, nearest_point)[1:]: + if _distance(points[-1], point) > DEFAULT_NODE_TOLERANCE: + points.append(point) + else: + points = _orthogonal_access_points(exit_point, nearest_point) if len(points) < 2: continue label = getattr(terminal, "Label", "") or getattr(terminal, "Name", "") or "Terminal" @@ -2206,12 +2422,18 @@ def create_routing_path_network_from_document( project_uuid=project_uuid, ) selected_wire_ducts = [] + selected_user_paths = [] if selection_ex: selected_wire_ducts = create_wire_duct_carriers_from_selection( doc, selection_ex, project_uuid=project_uuid, ) + selected_user_paths = create_user_path_carriers_from_selection( + doc, + selection_ex, + project_uuid=project_uuid, + ) wire_ducts = create_wire_duct_carriers_from_document( doc, project_uuid=project_uuid, @@ -2250,6 +2472,7 @@ def create_routing_path_network_from_document( return { "wire_duct_carriers": wire_duct_main_count, "selected_wire_duct_carriers": selected_wire_duct_main_count, + "user_path_carriers": len(selected_user_paths), "wire_duct_open_end_carriers": open_end_count, "wiring_cut_out_carriers": len(cut_outs), "surface_carriers": len(surfaces), @@ -2273,6 +2496,8 @@ def create_wire_duct_carriers_from_selection( cleanup_invalid_source_carriers(doc) created = [] for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1): + if _source_kind_value(source) == ROUTE_CARRIER_KIND_USER_PATH or _is_route_path_source_object(source): + continue bbox = _bound_box_from_object(source) if bbox is None: continue diff --git a/src/Mod/FreeCADExchange/TemplateAuthoring.py b/src/Mod/FreeCADExchange/TemplateAuthoring.py index 1b8fa9e..39ad758 100644 --- a/src/Mod/FreeCADExchange/TemplateAuthoring.py +++ b/src/Mod/FreeCADExchange/TemplateAuthoring.py @@ -1,5 +1,7 @@ # FreeCADExchange FCStd template authoring helpers. +import json + import FreeCAD as App try: @@ -110,6 +112,151 @@ def is_template_terminal(obj): return TerminalObjects.is_terminal_hint_object(obj) +def _route_point_payload(point): + if isinstance(point, App.Vector): + return [float(point.x), float(point.y), float(point.z)] + if isinstance(point, dict): + try: + return [ + float(point.get("x", 0.0) or 0.0), + float(point.get("y", 0.0) or 0.0), + float(point.get("z", 0.0) or 0.0), + ] + except Exception: + return None + if isinstance(point, (list, tuple)) and len(point) >= 3: + try: + return [float(point[0] or 0.0), float(point[1] or 0.0), float(point[2] or 0.0)] + except Exception: + return None + return None + + +def _distance(left, right): + try: + dx = float(left.x) - float(right.x) + dy = float(left.y) - float(right.y) + dz = float(left.z) - float(right.z) + return (dx * dx + dy * dy + dz * dz) ** 0.5 + except Exception: + return 0.0 + + +def _edge_points(edge): + vertexes = list(getattr(edge, "Vertexes", []) or []) + if len(vertexes) >= 2: + start = getattr(vertexes[0], "Point", None) + end = getattr(vertexes[-1], "Point", None) + if start is not None and end is not None: + return [start, end] + try: + return [edge.valueAt(edge.FirstParameter), edge.valueAt(edge.LastParameter)] + except Exception: + return [] + + +def _selection_item_points(selection_item): + points = [] + points.extend(list(getattr(selection_item, "PickedPoints", []) or [])) + for sub_object in list(getattr(selection_item, "SubObjects", []) or []): + shape_type = (getattr(sub_object, "ShapeType", "") or "").lower() + if shape_type == "edge": + points.extend(_edge_points(sub_object)) + continue + if shape_type == "vertex": + point = getattr(sub_object, "Point", None) + if point is not None: + points.append(point) + obj = getattr(selection_item, "Object", None) + shape = getattr(obj, "Shape", None) + if shape is not None and not is_template_terminal(obj): + for edge in list(getattr(shape, "Edges", []) or []): + points.extend(_edge_points(edge)) + + normalized = [] + for point in points: + if point is None: + continue + if not normalized or _distance(normalized[-1], point) > 1e-6: + normalized.append(point) + return normalized + + +def _document_point_to_terminal_local(terminal, point): + placement = getattr(terminal, "Placement", None) + if placement is None: + return point + try: + inverse = placement.inverse() + transformed = inverse.multVec(point) + if transformed is not None: + return transformed + except Exception: + pass + base = getattr(placement, "Base", None) + if base is None: + return point + return App.Vector( + float(point.x) - float(base.x), + float(point.y) - float(base.y), + float(point.z) - float(base.z), + ) + + +def set_template_terminal_local_route_points(obj, points): + if not is_template_terminal(obj): + raise TemplateAuthoringError("A valid template terminal is required.") + payload = [] + for point in points or []: + item = _route_point_payload(point) + if item is not None: + payload.append(item) + if len(payload) < 2: + raise TemplateAuthoringError("At least two local route points are required.") + TerminalObjects.ensure_string_property( + obj, + "QetTerminalLocalRoutePointsJson", + TEMPLATE_PROPERTY_GROUP, + "Terminal-local route points used before joining the cabinet routing network", + json.dumps(payload, ensure_ascii=False), + ) + try: + obj.Document.recompute() + except Exception: + pass + return obj + + +def set_selected_template_terminal_local_route_points(selection_ex=None): + if selection_ex is None: + if Gui is None: + raise TemplateAuthoringError("FreeCAD GUI selection is not available.") + try: + selection_ex = Gui.Selection.getSelectionEx() + except Exception: + selection_ex = [] + + terminal = None + route_points = [] + for item in list(selection_ex or []): + obj = getattr(item, "Object", None) + if terminal is None and is_template_terminal(obj): + terminal = obj + continue + route_points.extend(_selection_item_points(item)) + + if terminal is None: + raise TemplateAuthoringError("Select one template terminal before selecting its local route path.") + if len(route_points) < 2: + raise TemplateAuthoringError("Select a sketch, edge, or route path with at least two points.") + + local_points = [ + _document_point_to_terminal_local(terminal, point) + for point in route_points + ] + return set_template_terminal_local_route_points(terminal, local_points) + + def _has_property(obj, prop_name): return prop_name in getattr(obj, "PropertiesList", []) @@ -276,6 +423,30 @@ class CommandValidateTemplateTerminals: App.Console.PrintWarning("[FreeCADExchange] {0}\n".format(warning)) +class CommandSetTemplateTerminalLocalRoute: + def GetResources(self): + return { + "MenuText": "设置端子局部路径", + "ToolTip": "把选中的草图或线段保存为模板端子的局部出线路径", + } + + def IsActive(self): + return App.ActiveDocument is not None and Gui is not None + + def Activated(self): + try: + terminal = set_selected_template_terminal_local_route_points() + App.Console.PrintMessage( + "[FreeCADExchange] Updated local route points for template terminal {0}.\n".format( + getattr(terminal, "QetTemplateSlotName", "") or getattr(terminal, "Name", "") + ) + ) + except Exception as exc: + App.Console.PrintError( + "[FreeCADExchange] setting template terminal local route failed: {0}\n".format(exc) + ) + + class CommandSaveTemplateAsFCStd: def GetResources(self): return { @@ -325,6 +496,7 @@ def register_commands(): return Gui.addCommand("QET_Template_AddTerminal", CommandAddTemplateTerminal()) Gui.addCommand("QET_Template_ValidateTerminals", CommandValidateTemplateTerminals()) + Gui.addCommand("QET_Template_SetTerminalLocalRoute", CommandSetTemplateTerminalLocalRoute()) Gui.addCommand("QET_Template_SaveAsFCStd", CommandSaveTemplateAsFCStd()) _COMMANDS_REGISTERED = True diff --git a/src/Mod/FreeCADExchange/TemplateSemantics.py b/src/Mod/FreeCADExchange/TemplateSemantics.py index b532df0..ff17ad4 100644 --- a/src/Mod/FreeCADExchange/TemplateSemantics.py +++ b/src/Mod/FreeCADExchange/TemplateSemantics.py @@ -123,6 +123,34 @@ def _vector_to_payload(value): return [float(vector.x), float(vector.y), float(vector.z)] +def _route_points_to_payload(value): + if not isinstance(value, (list, tuple)): + return None + points = [] + for item in value: + point = _vector_to_payload(item) + if point is not None: + points.append(point) + return points if points else None + + +def _route_points_from_object(source_object): + if source_object is None: + return None + for property_name in ("QetTerminalLocalRoutePointsJson", "QetLocalRoutePointsJson"): + raw_text = getattr(source_object, property_name, "") + if not isinstance(raw_text, str) or not raw_text.strip(): + continue + try: + parsed = json.loads(raw_text) + except (TypeError, ValueError): + continue + points = _route_points_to_payload(parsed) + if points: + return points + return None + + def _rotation_to_payload(value): if not isinstance(value, dict): return None @@ -161,6 +189,9 @@ def _slot_to_payload(slot): rotation = _rotation_to_payload(slot.get("rotation")) if rotation is not None: payload["rotation"] = rotation + local_route_points = _route_points_to_payload(slot.get("local_route_points")) + if local_route_points is not None: + payload["local_route_points"] = local_route_points return payload @@ -263,6 +294,18 @@ def _slot_from_payload(item, source, index, source_object=None): rotation = placement_rotation if rotation is None: rotation = _rotation_from_object(source_object) + local_route_points = None + for route_points_key in ( + "local_route_points", + "terminal_local_route_points", + "exit_path", + "route_points", + ): + local_route_points = _route_points_to_payload(item.get(route_points_key)) + if local_route_points is not None: + break + if local_route_points is None: + local_route_points = _route_points_from_object(source_object) slot = { "name": name, @@ -273,6 +316,8 @@ def _slot_from_payload(item, source, index, source_object=None): } if rotation is not None: slot["rotation"] = rotation + if local_route_points is not None: + slot["local_route_points"] = local_route_points return slot diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index e4b6435..88bed6e 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -1,6 +1,7 @@ # FreeCADExchange terminal import helpers. from collections import OrderedDict +import json import FreeCAD as App @@ -144,12 +145,8 @@ def _terminal_belongs_to_payload_devices(entry, device_lookup): return False -def _ensure_visible(obj): - try: - if getattr(obj, "ViewObject", None) is not None: - obj.ViewObject.Visibility = True - except Exception: - pass +def _hide_engineering_terminal(obj): + TerminalObjects.hide_engineering_terminal(obj) def _set_terminal_geometry_source(obj, source): @@ -167,6 +164,21 @@ def _set_terminal_geometry_source(obj, source): ) +def _set_terminal_local_route_points(obj, slot): + points = slot.get("local_route_points") if isinstance(slot, dict) else None + raw_text = "" + if isinstance(points, list) and points: + raw_text = json.dumps(points, ensure_ascii=False) + if raw_text or "QetTerminalLocalRoutePointsJson" in getattr(obj, "PropertiesList", []): + TerminalObjects.ensure_string_property( + obj, + "QetTerminalLocalRoutePointsJson", + "QET Routing", + "Terminal-local route points used before joining the cabinet routing network", + raw_text, + ) + + def _hide_object(obj): try: if getattr(obj, "ViewObject", None) is not None: @@ -339,7 +351,8 @@ def _create_terminal_object(doc, terminal_uuid, entry, slot, terminal_group, pro slot_name=slot.get("name", ""), ) _set_terminal_geometry_source(terminal_obj, slot.get("source", "template")) - _ensure_visible(terminal_obj) + _set_terminal_local_route_points(terminal_obj, slot) + _hide_engineering_terminal(terminal_obj) return terminal_obj @@ -510,11 +523,12 @@ def import_terminals_from_payload(payload, scene_path=""): slot_name=slot.get("name", ""), ) _set_terminal_geometry_source(terminal_obj, slot.get("source", "template")) + _set_terminal_local_route_points(terminal_obj, slot) try: terminal_obj.Placement = _slot_placement(slot) except Exception: pass - _ensure_visible(terminal_obj) + _hide_engineering_terminal(terminal_obj) report["updated_terminals"] += 1 else: terminal_obj = _create_terminal_object( @@ -539,11 +553,12 @@ def import_terminals_from_payload(payload, scene_path=""): slot_name=slot.get("name", ""), ) _set_terminal_geometry_source(terminal_obj, slot.get("source", "template")) + _set_terminal_local_route_points(terminal_obj, slot) try: terminal_obj.Placement = _slot_placement(slot) except Exception: pass - _ensure_visible(terminal_obj) + _hide_engineering_terminal(terminal_obj) report["updated_terminals"] += 1 if terminal_obj not in getattr(terminal_group, "Group", []): diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 923a858..5ab3b89 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -662,6 +662,57 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual((0.65, 0.2, 1.0), terminal_access.ViewObject.LineColor) self.assertEqual("Solid", terminal_access.ViewObject.DrawStyle) + def test_set_route_carriers_visibility_toggles_only_route_helpers(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, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + ) + device = doc.addObject("Part::Feature", "DeviceA") + device.ViewObject.Visibility = True + + hidden = routing_network.set_route_carriers_visibility(doc, False) + self.assertFalse(carrier.ViewObject.Visibility) + shown = routing_network.set_route_carriers_visibility(doc, True) + + self.assertEqual(1, hidden) + self.assertEqual(1, shown) + self.assertTrue(carrier.ViewObject.Visibility) + self.assertTrue(device.ViewObject.Visibility) + + def test_collect_route_carriers_ignores_deleted_object_references(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, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + ) + + class DeletedObjectReference: + Name = "DeletedCarrier" + + def __getattr__(self, name): + if name == "QetRoutingRole": + raise RuntimeError("Cannot access attribute 'QetRoutingRole' of deleted object") + raise AttributeError(name) + + doc.Objects.append(DeletedObjectReference()) + + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual([carrier], carriers) + def test_route_carrier_exposes_capacity_property_for_auto_routing(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -1141,6 +1192,182 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, result["wire_duct_carriers"]) self.assertEqual("selection", result["source_mode"]) + def test_generate_routing_paths_uses_selected_route_path_as_user_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") + route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path.Label = "用户主路径A" + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[ + FakeEdge(app.Vector(0, 0, 20), app.Vector(0, 80, 20)), + FakeEdge(app.Vector(0, 80, 20), app.Vector(100, 80, 20)), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = routing_network.collect_route_carriers(doc) + user_paths = [item for item in carriers if item.QetRouteCarrierKind == "UserPath"] + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(1, len(user_paths)) + self.assertEqual("UserRouteSketch", user_paths[0].QetRouteSourceName) + self.assertEqual("用户主路径A", user_paths[0].QetRouteSourceLabel) + self.assertEqual("selection", result["source_mode"]) + + def test_controller_creates_selected_user_paths_without_full_network_generation(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))], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(1, result["network"]["kinds"]["UserPath"]) + self.assertEqual(1, len(carriers)) + self.assertEqual("UserPath", carriers[0].QetRouteCarrierKind) + + 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") + 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: [], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + + 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_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 = _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(10, 80, 0), app.Vector(110, 80, 0)], + project_uuid="project-1", + 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)) + 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_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))], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + 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, first["user_path_carriers"]) + self.assertEqual(0, 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_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(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="UserPath", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual("Routed", result["route_status"]) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + 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() @@ -1814,6 +2041,39 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("z", report["routes"][1]["lane"]["axis"]) self.assertEqual(8.0, report["routes"][1]["lane"]["offset_mm"]) + 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() + 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() @@ -2208,6 +2468,151 @@ class AutoRoutingTest(unittest.TestCase): self.assertTrue(report["missing_endpoint_samples"][0]["start_found"]) self.assertFalse(report["missing_endpoint_samples"][0]["end_found"]) + 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(100, 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_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_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) + + 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_writes_diagnostic_object_for_missing_terminal(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -2236,6 +2641,57 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) + 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(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_route_eplan_connections_reports_total_connection_route_length(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -2268,6 +2724,164 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"]) self.assertIn("总长度", message) + def test_route_eplan_connections_hides_route_carriers_after_routing_by_default(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)) + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=False, + ) + + self.assertEqual(1, report["routed"]) + self.assertEqual(1, report["hidden_route_carriers"]) + self.assertFalse(carrier.ViewObject.Visibility) + + def test_route_eplan_connections_batch_recomputes_once_after_created_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 10, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 10, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + recompute_count = {"value": 0} + + def count_recompute(): + recompute_count["value"] += 1 + + doc.recompute = count_recompute + 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) + + self.assertEqual(2, report["routed"]) + self.assertEqual(1, recompute_count["value"]) + + def test_route_eplan_connections_replaces_existing_routed_wires_for_same_batch(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-repeat", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + + first = auto_routing.route_eplan_connections_from_payload(doc, payload) + second = auto_routing.route_eplan_connections_from_payload(doc, payload) + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual(1, first["routed"]) + self.assertEqual(1, second["routed"]) + self.assertEqual(1, second["replaced_routed_connections"]) + self.assertEqual(1, len(routed_wires)) + self.assertEqual("wire-repeat", routed_wires[0].QetWireUuid) + + def test_clear_routing_connections_resets_task_status_and_batch_diagnostics(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _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", + ) + task = wiring_objects.create_wire_task( + doc, + "project-1", + "wire-clear", + "N1", + "terminal-start", + "terminal-end", + "instance-a", + "instance-b", + ) + + report = auto_routing.route_eplan_connection_tasks(doc) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + + self.assertEqual(1, report["routed"]) + self.assertEqual("Routed", task.RouteStatus) + self.assertEqual(1, len(list(getattr(diagnostic_group, "Group", []) or []))) + + removed = auto_routing.clear_routing_connections(doc) + + self.assertEqual(1, removed) + self.assertEqual("Task", task.RouteStatus) + self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + self.assertEqual([], list(getattr(diagnostic_group, "Group", []) or [])) + def test_route_report_includes_route_source_sample_when_available(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -2332,6 +2946,69 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("并行错位:最大 lane 2,间距 10.0 mm。", message) + def test_route_report_includes_replaced_routed_connection_count(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "replaced_routed_connections": 2, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("已替换旧布线连接:2 条。", message) + + def test_route_report_includes_hidden_route_carrier_count(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "hidden_route_carriers": 3, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("已隐藏走线路径辅助对象:3 条。", message) + + def test_route_report_warns_when_routes_use_surface_or_auxiliary_paths(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_label": "N1", + "route_track": { + "carrier_kinds": { + "TerminalAccess": 2, + "WireDuct": 1, + "RoutingRange": 2, + }, + }, + }, + { + "wire_label": "N2", + "route_track": { + "carrier_kinds": { + "TerminalAccess": 2, + "WireDuct": 3, + }, + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径质量提示:1 条导线使用布线面/辅助路径", message) + self.assertIn("示例 N1 使用布线面。", message) + def test_route_report_warns_when_parallel_lanes_exceed_track_capacity(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -2737,6 +3414,7 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(0, report["routes"][0]["lane"]["index"]) self.assertEqual(1, report["routes"][1]["lane"]["index"]) routed_group = doc.getObject("QETWiring_04_Routed") + self.assertEqual(2, len(list(getattr(routed_group, "Group", []) or []))) second_wire = [ wire for wire in list(getattr(routed_group, "Group", []) or []) @@ -3101,6 +3779,25 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) self.assertIn("缺失示例:terminal-a -> terminal-b", message) + def test_route_eplan_connections_report_calls_out_missing_route_network(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 3, + "routed": 0, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 3, + "route_status_counts": { + "MissingRouteNetwork": 3, + }, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("缺少布线路径网络 3 条", message) + self.assertIn("请先生成线槽、布线面或布线路径网络", message) + def test_route_eplan_connections_report_includes_readable_missing_endpoint_labels(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -3249,6 +3946,8 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(0, report["local_terminals"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual("qet", indexed["qet-terminal-p1"].QetTerminalBindingMode) + self.assertFalse(indexed["qet-terminal-p1"].ViewObject.Visibility) + self.assertFalse(indexed["qet-terminal-p2"].ViewObject.Visibility) def test_route_eplan_connections_rebinds_local_template_terminals_from_wire_endpoints(self): _install_fake_freecad() @@ -3324,6 +4023,8 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("qet-terminal-p1", indexed) self.assertIn("qet-terminal-p2", indexed) self.assertEqual("qet", indexed["qet-terminal-p1"].QetTerminalBindingMode) + self.assertFalse(indexed["qet-terminal-p1"].ViewObject.Visibility) + self.assertFalse(indexed["qet-terminal-p2"].ViewObject.Visibility) def test_clear_route_carriers_keeps_routed_wires(self): _install_fake_freecad() diff --git a/tests/python/freecad_exchange_device_import_fcstd_test.py b/tests/python/freecad_exchange_device_import_fcstd_test.py index 0326859..bbfcc2a 100644 --- a/tests/python/freecad_exchange_device_import_fcstd_test.py +++ b/tests/python/freecad_exchange_device_import_fcstd_test.py @@ -476,6 +476,8 @@ class FcstdDeviceImportTest(unittest.TestCase): terminal.Role = "Terminal" terminal.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "") terminal.QetTemplateSlotName = "D1" + terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Template", "") + terminal.QetTerminalLocalRoutePointsJson = "[[0,0,0],[6,0,0],[6,18,0]]" x_axis = source.addObject("App::Line", "Terminal_D1_XAxis") terminal.OriginFeatures = [x_axis] @@ -504,6 +506,10 @@ class FcstdDeviceImportTest(unittest.TestCase): self.assertEqual("D1", slots[0]["name"]) self.assertEqual(11.0, slots[0]["base"].x) self.assertEqual(90.0, slots[0]["rotation"]["angle"]) + self.assertEqual( + [[0.0, 0.0, 0.0], [6.0, 0.0, 0.0], [6.0, 18.0, 0.0]], + slots[0]["local_route_points"], + ) self.assertIsNone(slots[0]["source_object"]) def test_fcstd_import_keeps_link_dependencies_out_of_device_group(self): diff --git a/tests/python/freecad_exchange_template_authoring_test.py b/tests/python/freecad_exchange_template_authoring_test.py index 2c4a839..6556cc0 100644 --- a/tests/python/freecad_exchange_template_authoring_test.py +++ b/tests/python/freecad_exchange_template_authoring_test.py @@ -1,4 +1,5 @@ import importlib +import json import sys import types import unittest @@ -88,6 +89,30 @@ class FakeObject: self.Group.append(child) +class FakeVertex: + def __init__(self, point): + self.Point = point + + +class FakeEdge: + ShapeType = "Edge" + + def __init__(self, start, end): + self.Vertexes = [FakeVertex(start), FakeVertex(end)] + + +class FakeShape: + def __init__(self, edges=None): + self.Edges = edges or [] + + +class FakeSelectionItem: + def __init__(self, obj=None, sub_objects=None, picked_points=None): + self.Object = obj + self.SubObjects = sub_objects or [] + self.PickedPoints = picked_points or [] + + class FakeDocument: def __init__(self): self.Name = "TemplateDoc" @@ -132,6 +157,10 @@ class TemplateAuthoringTest(unittest.TestCase): "校验模板端子", template_authoring.CommandValidateTemplateTerminals().GetResources()["MenuText"], ) + self.assertEqual( + "设置端子局部路径", + template_authoring.CommandSetTemplateTerminalLocalRoute().GetResources()["MenuText"], + ) self.assertEqual( "保存模板为 FCStd", template_authoring.CommandSaveTemplateAsFCStd().GetResources()["MenuText"], @@ -169,6 +198,54 @@ class TemplateAuthoringTest(unittest.TestCase): self.assertEqual(30.0, terminal.Placement.Base.z) self.assertTrue(doc.recomputed) + def test_set_template_terminal_local_route_points_writes_json_property(self): + _install_fake_freecad() + template_authoring = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal = template_authoring.create_template_terminal(doc, "P1", app.Vector(1, 2, 3)) + + template_authoring.set_template_terminal_local_route_points( + terminal, + [ + app.Vector(0, 0, 0), + [8, 0, 0], + {"x": 8, "y": 20, "z": 0}, + ], + ) + + self.assertIn("QetTerminalLocalRoutePointsJson", terminal.PropertiesList) + self.assertEqual( + [[0.0, 0.0, 0.0], [8.0, 0.0, 0.0], [8.0, 20.0, 0.0]], + json.loads(terminal.QetTerminalLocalRoutePointsJson), + ) + + def test_set_selected_template_terminal_local_route_points_uses_terminal_local_coordinates(self): + _install_fake_freecad() + template_authoring = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal = template_authoring.create_template_terminal(doc, "P1", app.Vector(10, 0, 0)) + path = doc.addObject("Part::Feature", "LocalExitPath") + path.Shape = FakeShape( + edges=[ + FakeEdge(app.Vector(10, 0, 0), app.Vector(20, 0, 0)), + FakeEdge(app.Vector(20, 0, 0), app.Vector(20, 15, 0)), + ] + ) + + template_authoring.set_selected_template_terminal_local_route_points( + [ + FakeSelectionItem(obj=terminal), + FakeSelectionItem(obj=path), + ] + ) + + self.assertEqual( + [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 15.0, 0.0]], + json.loads(terminal.QetTerminalLocalRoutePointsJson), + ) + def test_validate_template_terminals_reports_missing_slot_name(self): _install_fake_freecad() template_authoring = _reload_modules() diff --git a/tests/python/freecad_exchange_template_semantics_test.py b/tests/python/freecad_exchange_template_semantics_test.py index 444cf25..6292f63 100644 --- a/tests/python/freecad_exchange_template_semantics_test.py +++ b/tests/python/freecad_exchange_template_semantics_test.py @@ -153,6 +153,40 @@ class TemplateSemanticsRotationTest(unittest.TestCase): self.assertEqual(0.0, slots[0]["rotation"]["axis"].y) self.assertEqual(1.0, slots[0]["rotation"]["axis"].z) + def test_sidecar_keeps_terminal_local_route_points(self): + _install_fake_freecad() + template_semantics, _ = _reload_exchange_modules() + + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "Relay.step" + model_path.write_text("", encoding="utf-8") + sidecar_path = Path(temp_dir) / "Relay.qet_template.json" + sidecar_path.write_text( + json.dumps( + { + "terminal_slots": [ + { + "name": "A1", + "position": [10, 20, 30], + "local_route_points": [ + [0, 0, 0], + {"x": 12, "y": 0, "z": 0}, + [12, 25, 0], + ], + } + ] + } + ), + encoding="utf-8", + ) + + slots = template_semantics.load_sidecar_terminal_slots(str(model_path)) + + self.assertEqual( + [[0.0, 0.0, 0.0], [12.0, 0.0, 0.0], [12.0, 25.0, 0.0]], + slots[0]["local_route_points"], + ) + class TerminalSlotResolutionPolicyTest(unittest.TestCase): def test_resolve_terminal_slots_returns_bbox_fallback_when_model_has_no_template_slots(self): 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 7636713..d6e09b1 100644 --- a/tests/python/freecad_exchange_terminal_import_template_slots_test.py +++ b/tests/python/freecad_exchange_terminal_import_template_slots_test.py @@ -1,4 +1,5 @@ import importlib +import json import sys import types import unittest @@ -191,6 +192,7 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual(1, len(terminals)) self.assertEqual("terminal-a", terminals[0].QetTerminalUuid) self.assertEqual("generated_bbox_fallback", terminals[0].QetTerminalGeometrySource) + self.assertFalse(terminals[0].ViewObject.Visibility) def test_import_preserves_local_terminals_when_payload_has_no_entry_for_device(self): _install_fake_freecad() @@ -524,6 +526,91 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual(2, len(terminals)) self.assertEqual(20.0, terminals["terminal-p2"].Placement.Base.x) self.assertEqual(10.0, terminals["terminal-p1"].Placement.Base.x) + self.assertFalse(terminals["terminal-p2"].ViewObject.Visibility) + self.assertFalse(terminals["terminal-p1"].ViewObject.Visibility) + + def test_import_copies_template_slot_local_route_points_to_terminal(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "instance-a", + ) + terminal_objects.ensure_string_property( + device, + "QetTemplateSlotsJson", + "QET Exchange", + "Stored template slots", + json.dumps( + { + "terminal_slots": [ + { + "name": "P1", + "label": "P1", + "base": [10, 0, 0], + "local_route_points": [[0, 0, 0], [8, 0, 0], [8, 20, 0]], + } + ] + } + ), + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "element_uuid": "device-a", + "instance_id": "instance-a", + } + ], + "terminals": [ + { + "terminal_uuid": "terminal-p1", + "element_uuid": "device-a", + "instance_id": "instance-a", + "terminal_display": "P1", + }, + ], + } + ) + + terminal_group = terminal_objects.find_child_group_by_kind( + device, + terminal_objects.TERMINAL_GROUP_KIND, + ) + terminals = terminal_objects.collect_terminal_objects(terminal_group) + + self.assertEqual(1, report["imported_terminals"]) + self.assertEqual(1, len(terminals)) + self.assertIn("QetTerminalLocalRoutePointsJson", terminals[0].PropertiesList) + self.assertEqual( + [[0.0, 0.0, 0.0], [8.0, 0.0, 0.0], [8.0, 20.0, 0.0]], + json.loads(terminals[0].QetTerminalLocalRoutePointsJson), + ) def test_import_rebinds_existing_local_terminal_on_matching_template_slot(self): _install_fake_freecad() @@ -619,6 +706,7 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual("terminal-real-p1", local_terminal.QetTerminalUuid) self.assertEqual("instance-a", local_terminal.QetInstanceId) self.assertEqual(15.0, local_terminal.Placement.Base.x) + self.assertFalse(local_terminal.ViewObject.Visibility) if __name__ == "__main__": From b0c662bd11ff51d4b7be20640b9750a6bd371f0d Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Tue, 2 Jun 2026 16:15:28 +0800 Subject: [PATCH 55/63] feat(freecad): improve qet assembly placement workflow --- docs/FreeCAD 机柜装配操作文档.md | 6 +- .../2026-05-29-face-contact-snap-design.md | 25 +- ...06-02-batch-din-device-placement-design.md | 89 +++ src/Mod/FreeCADExchange/BatchAssembly.py | 515 +++++++++++++++++ src/Mod/FreeCADExchange/CMakeLists.txt | 1 + src/Mod/FreeCADExchange/ManualWiringPanel.py | 516 +++++++++++++++++- .../FreeCADExchange/TemplateInstantiation.py | 2 +- src/Mod/FreeCADExchange/TerminalObjects.py | 11 + .../freecad_exchange_batch_assembly_test.py | 260 +++++++++ ...eecad_exchange_manual_wiring_panel_test.py | 388 +++++++++++++ ...ad_exchange_template_instantiation_test.py | 1 + 11 files changed, 1801 insertions(+), 13 deletions(-) create mode 100644 docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md create mode 100644 src/Mod/FreeCADExchange/BatchAssembly.py create mode 100644 tests/python/freecad_exchange_batch_assembly_test.py diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index afa1043..1a6a638 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -381,8 +381,10 @@ Z = 1200 mm 1. FreeCAD 原生 `Assembly` 工作台可以做平面对齐、距离、同轴/共线等配合关系。 2. FreeCADExchange 目前主要保存对象最终 `Placement`,并提供轻量的 `贴合到选中面` 辅助。 3. `贴合到选中面` 是一次性位姿调整,不是持久 Assembly 约束求解器。 -4. 当前不会自动保存“设备装在哪根导轨上”“导轨固定在哪块安装板上”这类完整宿主关系。 -5. 自动布线读取 `scene.FCStd` 里的最终几何位置,不从装配约束反推位置。 +4. 执行 `贴合到选中面` 后,被移动对象会记录一组轻量宿主元数据:`QetMountMode`、`QetMountKind`、`QetMountHostName`、`QetMountHostKind`、`QetMountHostSubElement`、`QetMountContactSubElement`。这些信息保存在 FreeCAD 文档对象属性中,不写入第一版数据库。 +5. `3D手动布线` 面板提供 `刷新宿主装配`。宿主对象平移后,已记录宿主关系的导轨、线槽或设备会按保存的局部偏移跟随更新。 +6. 当前 `刷新宿主装配` 只处理平移联动,不处理宿主旋转、导轨槽位约束或复杂 Assembly 求解。 +7. 自动布线读取 `scene.FCStd` 里的最终几何位置,不从装配约束反推位置。 推荐建模习惯: diff --git a/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md b/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md index 19bbbd3..a1b0bdf 100644 --- a/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md +++ b/docs/superpowers/specs/2026-05-29-face-contact-snap-design.md @@ -24,10 +24,22 @@ CAD 用户在 FreeCAD 中摆放导轨、线槽和设备时,需要让两个接 ## 交互 -1. 在 3D 视图中选择机柜、导轨或线槽上的目标面。 -2. 按住 Ctrl,再选择要移动对象上的接触面。 -3. 点击 `贴合到选中面`。 -4. 如果方向不理想,用户可以先用 FreeCAD `变换` 粗调姿态,再重新执行贴合。 +推荐流程: + +1. 在 3D 视图中选择机柜、导轨或线槽上的目标安装面。 +2. 点击 `设为贴合目标面`。 +3. 选择要移动对象上的接触面。 +4. 点击 `贴合到选中面`。 +5. 对同一个目标面连续摆放多个设备时,只重复第 3、4 步。 + +兼容流程: + +1. 同时选择两个面,第一个是目标面,第二个是要移动对象的接触面。 +2. 点击 `贴合到选中面`。 + +如果用户只选中了一个对象而没有选中具体面,系统会尝试使用该对象面积最大的平面作为贴合面。这是为了降低机柜板、导轨、线槽这类规则模型的选面难度;复杂设备仍建议精确选择真实安装面。 + +如果方向不理想,用户可以先用 FreeCAD 旋转视图、隐藏遮挡物或透明化对象来选面。不要为了选面而旋转模型本体。 第一版只接受两个面。多选多个设备、多个面时,系统不能唯一判断哪个对象应该移动、哪个面是目标、是否要同时满足多个约束,因此会直接提示错误。多面贴合属于完整 Assembly 约束求解范围,不放进这个轻量按钮。 @@ -39,9 +51,14 @@ CAD 用户在 FreeCAD 中摆放导轨、线槽和设备时,需要让两个接 - 多于两个面:提示只能选择两个面。 - 第二个选择对象没有可移动 `Placement`:提示对象不能移动。 - 无法读取面中心或法向:提示请选择有效模型面。 +- 已设置目标面后又选中多个移动面:提示只选择一个接触面。 ## 测试 - 选择两个面后,移动对象应只沿目标面法向平移,消除法向间距。 - 多选三个或更多面时应报错。 +- 已设置目标面后,只选择一个接触面即可贴合。 +- 选择内部子零件面时,应移动 QET 设备或载体根对象,而不是只移动内部 Shape。 +- 贴合后应重新选中被移动的根对象,保证后续 FreeCAD `变换` 从新坐标开始。 +- 只选对象时,可用最大平面作为辅助贴合面。 - 导入类操作或贴合操作后,`App.ActiveDocument` 仍应是当前 QET 工程。 diff --git a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md new file mode 100644 index 0000000..201bdd2 --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md @@ -0,0 +1,89 @@ +# 批量端子排与小型断路器装配设计 + +## 目标 + +第一版只做当前项目可演示、可操作的批量装配能力:用户在 FreeCAD 中选中一根已标记的导轨后,可以批量插入端子片形成端子排,也可以批量插入小型断路器。系统按导轨方向等距放置对象,并为每个对象生成可布线工程端子。 + +本功能不做完整设备库、不扩展数据库绑定表、不替代 QET 现有 2D 设备/符号关联。2D 仍负责设备型号、端子号和导线任务;3D 只负责模型、位姿、端子空间点和装配状态。 + +## 范围 + +### 端子排 + +用户选择导轨后,输入端子排名称、端子数量、端子片宽度/间距和起始偏移。系统沿导轨方向生成: + +- 一个端子排分组,例如 `XT1` +- 多个端子片实例,例如 `XT1_001`、`XT1_002`。这些实例同时写入标准 QET 设备语义,便于后续端子绑定和布线索引识别。 +- 每片端子对应的工程端子,例如 `XT1:1`、`XT1:2` + +正式主流程中,端子片模型资源应来自 QET 传入的设备/资产绑定。后端保留 `model_path` 参数,用于 QET 自动传入模型路径或开发调试兜底;普通用户参数窗口不要求手动选择模型文件。未传入模型路径时回退为脚本生成的简化几何。关键是位置整齐、命名清楚、可被布线模块发现。 + +### 小型断路器 + +用户选择导轨后,输入起始设备名、数量、单个宽度/间距和端子号模板。系统沿导轨方向生成: + +- 多个设备实例,例如 `QF1`、`QF2`。对象名称使用 `QETDevice_QF1` 这类标准前缀,树目录 Label 仍显示 `QF1`。 +- 每个设备的工程端子,例如 `QF1:1`、`QF1:2`、`QF1:3`、`QF1:4`、`QF1:5`、`QF1:6` + +如果 2D 已经提供真实设备名和端子号,后续导入/绑定逻辑优先使用 QET 的 `terminal_uuid`;本功能生成的是第一版本地 3D 辅助对象,用于快速摆放和演示。 + +## 数据语义 + +- 不新增数据库字段。 +- 3D 对象保存在 FreeCAD 文档中。 +- 工程端子仍使用 `TerminalObjects.set_terminal_semantics(...)`。 +- 批量生成的本地槽位端子使用 `local::`,避免伪造 QET terminal_uuid。 +- 批量设备组写入 `QetProjectUuid`、`QetElementUuid`、`QetInstanceId` 和 `QetGroupKind=Device`,使它们能被现有端子导入/布线绑定逻辑找到。 +- 当 QET 的 `2d_to_3d.json` 后续提供真实 `terminal_uuid + terminal_display` 时,布线/端子绑定逻辑按端子号匹配本地槽位,并把 `local:*` 提升为真实 QET `terminal_uuid`。 +- 树目录显示名使用 `设备名:端子号`,方便设计人员辨认。 +- 批量对象额外写入本地属性: +- `QetBatchAssemblyKind` +- `QetBatchAssemblyName` +- `QetMountHostName` +- `QetMountKind` +- `QetBatchSourceModelPath`,仅导入本地模型文件时写入可见几何对象 + +## 导轨定位规则 + +第一版使用导轨对象的 `QetCarrierAxis` 作为排列轴,默认 `x`。放置公式: + +```text +第 N 个对象位置 = 导轨 Placement.Base + 轴向单位向量 * (起始偏移 + N * 间距) +``` + +如果导轨对象带有旋转,排列轴会经过导轨 `Placement.Rotation` 转换。第一版先保证当前工程和内置导轨的稳定演示。 + +## UI + +挂在 `QET模板 -> 3D手动布线` 面板,新增两个按钮: + +- `批量端子排` +- `批量断路器` + +点击按钮后弹出参数窗口,窗口内带默认参数: + +- 端子排:`XT1`,10 片,5.2 mm 间距 +- 小型断路器:`QF`,3 个,18 mm 间距,端子号 `1,2,3,4,5,6` + +端子号支持用空格、英文逗号、中文逗号、分号分隔;重复端子号会被拒绝,避免生成两个同名接线点。 +普通用户窗口不提供模型文件选择;模型文件由 QET 侧传入或由开发调试入口传入。不传入时使用脚本简化几何。 + +## 验收 + +1. 选中导轨后点击 `批量端子排`,生成 `XT1` 分组和端子片。 +2. 在参数窗口中可调整端子排名称、数量、间距和起始偏移。 +3. 每个端子片都有一个工程端子,Label 为 `XT1:n`。 +4. 选中导轨后点击 `批量断路器`,生成 `QF1`、`QF2`、`QF3`。 +5. 在参数窗口中可调整断路器前缀、数量、间距、起始偏移和端子号模板。 +6. 每个断路器生成指定端子号。 +7. 生成对象位于导轨方向上,间距正确。 +8. 端子默认隐藏,但可被手动/自动布线模块按端子对象收集。 +9. 如果 QET 已传入同一设备实例和端子号,`local:*` 槽位能被提升为真实 `terminal_uuid`,从而参与导线任务匹配。 + +## 非目标 + +- 不做完整 SolidWorks Electrical 设备库。 +- 不自动读取所有 2D 设备属性批量生成真实设备。 +- 不伪造 QET terminal_uuid;只有 QET 输入中存在真实端子 UUID 时才提升绑定。 +- 不做复杂 Assembly Joint。 +- 不做完整端子排电气跨接片、跳线、短接片规则。 diff --git a/src/Mod/FreeCADExchange/BatchAssembly.py b/src/Mod/FreeCADExchange/BatchAssembly.py new file mode 100644 index 0000000..ecd97d4 --- /dev/null +++ b/src/Mod/FreeCADExchange/BatchAssembly.py @@ -0,0 +1,515 @@ +import math +from pathlib import Path + +import FreeCAD as App + +import TerminalObjects + +try: + import ImportGui +except Exception: + ImportGui = None + + +class BatchAssemblyError(RuntimeError): + pass + + +def _project_uuid(doc): + try: + root = TerminalObjects.ensure_root_group(doc) + return (getattr(root, "QetProjectUuid", "") or "").strip() + except Exception: + return "" + + +def _safe_label(text, fallback): + value = str(text or "").strip() + return value or fallback + + +def _axis_vector(rail): + axis = (getattr(rail, "QetCarrierAxis", "") or "x").strip().lower() + if axis == "y": + vector = App.Vector(0, 1, 0) + elif axis == "z": + vector = App.Vector(0, 0, 1) + else: + vector = App.Vector(1, 0, 0) + + try: + placement = getattr(rail, "Placement", None) + rotation = getattr(placement, "Rotation", None) + if rotation is not None and hasattr(rotation, "multVec"): + vector = rotation.multVec(vector) + except Exception: + pass + + length = math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z) + if length <= 1e-9: + return App.Vector(1, 0, 0) + return App.Vector(vector.x / length, vector.y / length, vector.z / length) + + +def _base_point(rail): + placement = getattr(rail, "Placement", None) + base = getattr(placement, "Base", None) + if base is None: + return App.Vector(0, 0, 0) + return App.Vector(base.x, base.y, base.z) + + +def _point_at(base, axis, offset): + return App.Vector( + base.x + axis.x * float(offset or 0.0), + base.y + axis.y * float(offset or 0.0), + base.z + axis.z * float(offset or 0.0), + ) + + +def _placement_at(rail, point): + rotation = None + try: + rotation = getattr(getattr(rail, "Placement", None), "Rotation", None) + except Exception: + rotation = None + return App.Placement(point, rotation or App.Rotation()) + + +def _ensure_rail(rail): + if rail is None: + raise BatchAssemblyError("请先选择一根导轨。") + kind = (getattr(rail, "QetCarrierKind", "") or "").strip() + if kind and kind != "rail": + raise BatchAssemblyError("所选对象不是导轨。") + return rail + + +def _ensure_batch_root(doc, project_uuid=""): + group = doc.getObject("QETBatchAssembly") + if group is None: + group = doc.addObject("App::DocumentObjectGroup", "QETBatchAssembly") + group.Label = "QET Batch Assembly" + if project_uuid: + TerminalObjects.ensure_string_property( + group, + "QetProjectUuid", + "QET Batch Assembly", + "Project UUID", + project_uuid, + ) + return group + + +def _unique_object_name(doc, base_name): + name = TerminalObjects.safe_token(base_name) or "QETObject" + if doc.getObject(name) is None: + return name + + suffix = 1 + while doc.getObject("{0}_{1}".format(name, suffix)) is not None: + suffix += 1 + return "{0}_{1}".format(name, suffix) + + +def _existing_object_names(doc): + return {getattr(obj, "Name", "") for obj in list(getattr(doc, "Objects", []) or [])} + + +def _new_objects_since(doc, before_names): + return [ + obj + for obj in list(getattr(doc, "Objects", []) or []) + if getattr(obj, "Name", "") not in before_names + ] + + +def _top_level_objects(objects): + object_set = set(objects or []) + result = [] + for obj in objects or []: + parents = set(getattr(obj, "InList", []) or []) + if parents.intersection(object_set): + continue + result.append(obj) + return result + + +def _supported_model_path(path): + suffix = Path(path or "").suffix.lower() + return suffix in {".fcstd", ".step", ".stp"} + + +def _set_source_model_path(obj, model_path): + TerminalObjects.ensure_string_property( + obj, + "QetBatchSourceModelPath", + "QET Batch Assembly", + "Batch assembly imported model path", + model_path, + ) + + +def _import_fcstd_objects(doc, path): + if not hasattr(App, "openDocument") or not hasattr(doc, "copyObject"): + return [] + source_doc = None + try: + source_doc = App.openDocument(path, hidden=True, temporary=True) + copied = [] + for source_obj in _top_level_objects(list(getattr(source_doc, "Objects", []) or [])): + copied.append(doc.copyObject(source_obj, True)) + return copied + finally: + if source_doc is not None and hasattr(App, "closeDocument"): + try: + App.closeDocument(source_doc.Name) + except Exception: + pass + + +def _import_step_objects(doc, path): + if ImportGui is None: + return [] + before = _existing_object_names(doc) + try: + ImportGui.insert(name=path, docName=doc.Name, merge=False, useLinkGroup=True) + except TypeError: + ImportGui.insert(path, doc.Name) + return _top_level_objects(_new_objects_since(doc, before)) + + +def _import_model_objects(doc, model_path): + path = str(model_path or "").strip() + if not path: + return [] + if not _supported_model_path(path): + raise BatchAssemblyError("请选择 STEP/STP/FCStd 模型文件。") + if not Path(path).is_file(): + raise BatchAssemblyError("模型文件不存在:{0}".format(path)) + suffix = Path(path).suffix.lower() + if suffix == ".fcstd": + return _import_fcstd_objects(doc, path) + return _import_step_objects(doc, path) + + +def _set_batch_properties(obj, kind, batch_name, host): + TerminalObjects.ensure_string_property( + obj, + "QetBatchAssemblyKind", + "QET Batch Assembly", + "Batch assembly kind", + kind, + ) + TerminalObjects.ensure_string_property( + obj, + "QetBatchAssemblyName", + "QET Batch Assembly", + "Batch assembly name", + batch_name, + ) + TerminalObjects.ensure_string_property( + obj, + "QetMountKind", + "QET Mount", + "Mount kind", + "rail", + ) + TerminalObjects.ensure_string_property( + obj, + "QetMountHostName", + "QET Mount", + "Mount host object name", + getattr(host, "Name", "") or "", + ) + + +def _set_qet_device_properties(obj, project_uuid, element_uuid, instance_id): + TerminalObjects.ensure_string_property( + obj, + "QetGroupKind", + "QET Exchange", + "FreeCADExchange group kind", + "Device", + ) + TerminalObjects.ensure_string_property( + obj, + "QetProjectUuid", + "QET Exchange", + "Project UUID from QET exchange", + project_uuid, + ) + TerminalObjects.ensure_string_property( + obj, + "QetElementUuid", + "QET Exchange", + "Parent element UUID from QET exchange", + element_uuid, + ) + TerminalObjects.ensure_string_property( + obj, + "QetInstanceId", + "QET Exchange", + "Parent instance id from QET exchange", + instance_id, + ) + + +def _create_device_group( + doc, + parent, + label, + placement, + kind, + batch_name, + host, + project_uuid, + element_uuid, + instance_id, +): + group = doc.addObject( + "App::DocumentObjectGroup", + _unique_object_name(doc, "QETDevice_{0}".format(label)), + ) + group.Label = label + try: + group.Placement = placement + except Exception: + pass + parent.addObject(group) + _set_batch_properties(group, kind, batch_name, host) + _set_qet_device_properties(group, project_uuid, element_uuid, instance_id) + return group + + +def _create_visual_placeholder(doc, device_group, label, placement, kind): + try: + body = doc.addObject("Part::Feature", "QETBatchBody_{0}".format(TerminalObjects.safe_token(label))) + body.Label = "{0} 模型".format(label) + body.Placement = placement + try: + import Part + + if kind == "breaker": + body.Shape = Part.makeBox(18.0, 72.0, 80.0) + else: + body.Shape = Part.makeBox(5.2, 40.0, 45.0) + except Exception: + pass + try: + if kind == "breaker": + body.ViewObject.ShapeColor = (0.78, 0.78, 0.72) + else: + body.ViewObject.ShapeColor = (0.35, 0.75, 0.35) + except Exception: + pass + device_group.addObject(body) + _set_batch_properties(body, kind, getattr(device_group, "Label", "") or label, device_group) + return body + except Exception: + return None + + +def _create_visual_model(doc, device_group, label, placement, kind, model_path=""): + imported = _import_model_objects(doc, model_path) + if imported: + for index, obj in enumerate(imported): + try: + obj.Placement = placement + except Exception: + pass + if index == 0: + obj.Label = "{0} 模型".format(label) + device_group.addObject(obj) + _set_batch_properties(obj, kind, getattr(device_group, "Label", "") or label, device_group) + _set_source_model_path(obj, model_path) + return imported[0] + return _create_visual_placeholder(doc, device_group, label, placement, kind) + + +def _create_terminal( + doc, + device_group, + project_uuid, + element_uuid, + instance_id, + terminal_no, + offset_index=0, + label_prefix="", +): + owner_label = _safe_label(label_prefix, getattr(device_group, "Label", "") or instance_id) + label = "{0}:{1}".format(owner_label, terminal_no) + base = getattr(getattr(device_group, "Placement", None), "Base", App.Vector()) + terminal_point = App.Vector(base.x, base.y + float(offset_index) * 2.0, base.z) + terminal = TerminalObjects.create_lcs_object( + doc, + "QETTerminal_{0}_{1}".format(TerminalObjects.safe_token(instance_id), TerminalObjects.safe_token(terminal_no)), + placement=App.Placement(terminal_point, App.Rotation()), + label=label, + ) + terminal_group = TerminalObjects.ensure_terminal_group( + doc, + device_group, + project_uuid=project_uuid, + instance_id=instance_id, + ) + terminal_group.addObject(terminal) + TerminalObjects.set_terminal_semantics( + terminal, + project_uuid, + element_uuid, + "local:{0}:{1}".format(instance_id, terminal_no), + instance_id, + label=label, + slot_name=str(terminal_no), + ) + TerminalObjects.hide_engineering_terminal(terminal) + return terminal + + +def _batch_report(kind, group, devices, terminals): + return { + "kind": kind, + "group": group, + "devices": devices, + "terminals": terminals, + "created_devices": len(devices), + "created_terminals": len(terminals), + } + + +def create_terminal_block( + doc, + rail, + block_name="XT1", + count=10, + pitch_mm=5.2, + start_offset_mm=0.0, + model_path="", +): + if doc is None: + raise BatchAssemblyError("请先打开 FreeCAD 工程。") + rail = _ensure_rail(rail) + count = int(count or 0) + if count <= 0: + raise BatchAssemblyError("端子数量必须大于 0。") + + project_uuid = _project_uuid(doc) + batch_name = _safe_label(block_name, "XT1") + root = _ensure_batch_root(doc, project_uuid) + group = doc.addObject("App::DocumentObjectGroup", TerminalObjects.safe_token(batch_name)) + group.Label = batch_name + root.addObject(group) + _set_batch_properties(group, "terminal_block", batch_name, rail) + + base = _base_point(rail) + axis = _axis_vector(rail) + devices = [] + terminals = [] + for index in range(count): + terminal_no = str(index + 1) + point = _point_at(base, axis, float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0)) + device_label = "{0}_{1:03d}".format(batch_name, index + 1) + device = _create_device_group( + doc, + group, + device_label, + _placement_at(rail, point), + "terminal_slice", + batch_name, + rail, + project_uuid, + device_label, + device_label, + ) + _create_visual_model(doc, device, device_label, _placement_at(rail, point), "terminal_slice", model_path) + instance_id = device_label + element_uuid = batch_name + terminal = _create_terminal( + doc, + device, + project_uuid, + element_uuid, + instance_id, + terminal_no, + label_prefix=batch_name, + ) + devices.append(device) + terminals.append(terminal) + + try: + doc.recompute() + except Exception: + pass + return _batch_report("terminal_block", group, devices, terminals) + + +def create_breakers( + doc, + rail, + base_name="QF", + count=3, + pitch_mm=18.0, + start_offset_mm=0.0, + terminal_numbers=("1", "2", "3", "4", "5", "6"), + model_path="", +): + if doc is None: + raise BatchAssemblyError("请先打开 FreeCAD 工程。") + rail = _ensure_rail(rail) + count = int(count or 0) + if count <= 0: + raise BatchAssemblyError("断路器数量必须大于 0。") + terminal_numbers = [str(item).strip() for item in terminal_numbers or () if str(item).strip()] + if not terminal_numbers: + raise BatchAssemblyError("至少需要一个端子号。") + + project_uuid = _project_uuid(doc) + batch_name = _safe_label(base_name, "QF") + root = _ensure_batch_root(doc, project_uuid) + group = doc.addObject("App::DocumentObjectGroup", "QETBatch_{0}".format(TerminalObjects.safe_token(batch_name))) + group.Label = "{0} 批量断路器".format(batch_name) + root.addObject(group) + _set_batch_properties(group, "breaker_batch", batch_name, rail) + + base = _base_point(rail) + axis = _axis_vector(rail) + devices = [] + terminals = [] + for index in range(count): + device_label = "{0}{1}".format(batch_name, index + 1) + point = _point_at(base, axis, float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0)) + device = _create_device_group( + doc, + group, + device_label, + _placement_at(rail, point), + "breaker", + batch_name, + rail, + project_uuid, + device_label, + device_label, + ) + _create_visual_model(doc, device, device_label, _placement_at(rail, point), "breaker", model_path) + instance_id = device_label + element_uuid = device_label + for terminal_index, terminal_no in enumerate(terminal_numbers): + terminals.append( + _create_terminal( + doc, + device, + project_uuid, + element_uuid, + instance_id, + terminal_no, + offset_index=terminal_index, + ) + ) + devices.append(device) + + try: + doc.recompute() + except Exception: + pass + return _batch_report("breaker_batch", group, devices, terminals) diff --git a/src/Mod/FreeCADExchange/CMakeLists.txt b/src/Mod/FreeCADExchange/CMakeLists.txt index f1520fa..62207d8 100644 --- a/src/Mod/FreeCADExchange/CMakeLists.txt +++ b/src/Mod/FreeCADExchange/CMakeLists.txt @@ -21,6 +21,7 @@ set(FreeCADExchange_Scripts ExchangeWriteBack.py ManualWiring.py ManualWiringPanel.py + BatchAssembly.py ) set(FreeCADExchange_CabinetAssetDir diff --git a/src/Mod/FreeCADExchange/ManualWiringPanel.py b/src/Mod/FreeCADExchange/ManualWiringPanel.py index 3da71e3..19c4716 100644 --- a/src/Mod/FreeCADExchange/ManualWiringPanel.py +++ b/src/Mod/FreeCADExchange/ManualWiringPanel.py @@ -2,6 +2,7 @@ import json import math +import re from pathlib import Path import FreeCAD as App @@ -25,6 +26,7 @@ except ImportError: import ManualWiring import TerminalObjects import WiringObjects +import BatchAssembly try: import ExchangeWriteBack @@ -40,6 +42,13 @@ except Exception: COMMAND_NAME = "QET_Exchange_OpenManualWiringPanel" DEFAULT_TERMINAL_EXIT_LENGTH = 20.0 DEFAULT_CARRIER_BASE_LENGTH = 200.0 +DEFAULT_BATCH_TERMINAL_BLOCK_NAME = "XT1" +DEFAULT_BATCH_TERMINAL_BLOCK_COUNT = 10 +DEFAULT_BATCH_TERMINAL_BLOCK_PITCH = 5.2 +DEFAULT_BATCH_BREAKER_BASE_NAME = "QF" +DEFAULT_BATCH_BREAKER_COUNT = 3 +DEFAULT_BATCH_BREAKER_PITCH = 18.0 +DEFAULT_BATCH_BREAKER_TERMINALS_TEXT = "1,2,3,4,5,6" CARRIER_ROLE_LABELS = { "wire_duct": "线槽", "cabinet": "柜面", @@ -55,6 +64,157 @@ class ManualWiringPanelError(RuntimeError): pass +def _positive_int(value, field_label): + try: + result = int(value) + except Exception as exc: + raise ManualWiringPanelError("{0}必须是整数。".format(field_label)) from exc + if result <= 0: + raise ManualWiringPanelError("{0}必须大于 0。".format(field_label)) + return result + + +def _float_value(value, field_label): + try: + return float(value) + except Exception as exc: + raise ManualWiringPanelError("{0}必须是数字。".format(field_label)) from exc + + +def _positive_float(value, field_label): + result = _float_value(value, field_label) + if result <= 0: + raise ManualWiringPanelError("{0}必须大于 0。".format(field_label)) + return result + + +def _batch_name(value, fallback, field_label): + text = (str(value or "").strip() or fallback).strip() + if not text: + raise ManualWiringPanelError("{0}不能为空。".format(field_label)) + return text + + +def _parse_terminal_numbers_text(value): + parts = [item.strip() for item in re.split(r"[\s,,;;]+", str(value or "")) if item.strip()] + if not parts: + raise ManualWiringPanelError("端子号不能为空。") + seen = set() + for item in parts: + key = item.lower() + if key in seen: + raise ManualWiringPanelError("端子号不能重复:{0}".format(item)) + seen.add(key) + return tuple(parts) + + +def _batch_terminal_block_options(block_name, count, pitch_mm, start_offset_mm): + return { + "block_name": _batch_name(block_name, DEFAULT_BATCH_TERMINAL_BLOCK_NAME, "端子排名称"), + "count": _positive_int(count, "端子数量"), + "pitch_mm": _positive_float(pitch_mm, "端子间距"), + "start_offset_mm": _float_value(start_offset_mm, "起始偏移"), + } + + +def _batch_breaker_options(base_name, count, pitch_mm, start_offset_mm, terminal_numbers_text): + return { + "base_name": _batch_name(base_name, DEFAULT_BATCH_BREAKER_BASE_NAME, "断路器前缀"), + "count": _positive_int(count, "断路器数量"), + "pitch_mm": _positive_float(pitch_mm, "断路器间距"), + "start_offset_mm": _float_value(start_offset_mm, "起始偏移"), + "terminal_numbers": _parse_terminal_numbers_text(terminal_numbers_text), + } + + +def _exec_dialog(dialog): + if hasattr(dialog, "exec"): + return dialog.exec() + return dialog.exec_() + + +def _dialog_accepted_value(): + try: + return QtWidgets.QDialog.Accepted + except Exception: + return 1 + + +def _prompt_terminal_block_options(parent=None): + if QtWidgets is None: + raise ManualWiringPanelError("当前 FreeCAD 未加载 Qt,不能打开批量端子排参数窗口。") + dialog = QtWidgets.QDialog(parent) + dialog.setWindowTitle("批量端子排") + layout = QtWidgets.QFormLayout(dialog) + name_input = QtWidgets.QLineEdit(DEFAULT_BATCH_TERMINAL_BLOCK_NAME) + count_input = QtWidgets.QSpinBox() + count_input.setRange(1, 10000) + count_input.setValue(DEFAULT_BATCH_TERMINAL_BLOCK_COUNT) + pitch_input = QtWidgets.QDoubleSpinBox() + pitch_input.setRange(0.01, 10000.0) + pitch_input.setDecimals(2) + pitch_input.setValue(DEFAULT_BATCH_TERMINAL_BLOCK_PITCH) + offset_input = QtWidgets.QDoubleSpinBox() + offset_input.setRange(-100000.0, 100000.0) + offset_input.setDecimals(2) + offset_input.setValue(0.0) + layout.addRow("端子排名称", name_input) + layout.addRow("端子数量", count_input) + layout.addRow("端子间距 mm", pitch_input) + layout.addRow("起始偏移 mm", offset_input) + buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addRow(buttons) + if _exec_dialog(dialog) != _dialog_accepted_value(): + return None + return _batch_terminal_block_options( + name_input.text(), + count_input.value(), + pitch_input.value(), + offset_input.value(), + ) + + +def _prompt_breaker_options(parent=None): + if QtWidgets is None: + raise ManualWiringPanelError("当前 FreeCAD 未加载 Qt,不能打开批量断路器参数窗口。") + dialog = QtWidgets.QDialog(parent) + dialog.setWindowTitle("批量断路器") + layout = QtWidgets.QFormLayout(dialog) + name_input = QtWidgets.QLineEdit(DEFAULT_BATCH_BREAKER_BASE_NAME) + count_input = QtWidgets.QSpinBox() + count_input.setRange(1, 10000) + count_input.setValue(DEFAULT_BATCH_BREAKER_COUNT) + pitch_input = QtWidgets.QDoubleSpinBox() + pitch_input.setRange(0.01, 10000.0) + pitch_input.setDecimals(2) + pitch_input.setValue(DEFAULT_BATCH_BREAKER_PITCH) + offset_input = QtWidgets.QDoubleSpinBox() + offset_input.setRange(-100000.0, 100000.0) + offset_input.setDecimals(2) + offset_input.setValue(0.0) + terminals_input = QtWidgets.QLineEdit(DEFAULT_BATCH_BREAKER_TERMINALS_TEXT) + layout.addRow("断路器前缀", name_input) + layout.addRow("断路器数量", count_input) + layout.addRow("断路器间距 mm", pitch_input) + layout.addRow("起始偏移 mm", offset_input) + layout.addRow("端子号", terminals_input) + buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addRow(buttons) + if _exec_dialog(dialog) != _dialog_accepted_value(): + return None + return _batch_breaker_options( + name_input.text(), + count_input.value(), + pitch_input.value(), + offset_input.value(), + terminals_input.text(), + ) + + def _console_message(message): try: App.Console.PrintMessage("[FreeCADExchange] {0}\n".format(message)) @@ -245,9 +405,43 @@ def _selected_contact_face_refs(): } ) break + if refs: + return refs + + for obj in _selection(): + ref = _largest_object_face_ref(obj) + if ref is not None: + refs.append(ref) return refs +def _largest_object_face_ref(obj): + if obj is None: + return None + shape = getattr(obj, "Shape", None) + faces = list(getattr(shape, "Faces", []) or []) + candidates = [] + for face in faces: + if (getattr(face, "ShapeType", "") or "").strip().lower() != "face": + continue + point = _face_anchor_point(None, face) + normal = _face_normal(face) + if point is None or normal is None: + continue + candidates.append((float(getattr(face, "Area", 0.0) or 0.0), face, point, normal)) + if not candidates: + return None + + _area, face, point, normal = max(candidates, key=lambda item: item[0]) + return { + "object": obj, + "face": face, + "point": point, + "normal": normal, + "subelement_name": "LargestFace", + } + + def _has_placement(obj): return getattr(obj, "Placement", None) is not None @@ -350,6 +544,187 @@ def _translate_object(obj, translation): return new_base +def _placement_base(obj): + placement = getattr(obj, "Placement", None) + return getattr(placement, "Base", None) + + +def _set_placement_base(obj, base): + placement = getattr(obj, "Placement", None) + if placement is None or base is None: + return False + try: + placement.Base = base + obj.Placement = placement + return True + except Exception: + try: + obj.Placement = App.Placement(base, getattr(placement, "Rotation", App.Rotation())) + return True + except Exception: + return False + + +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 _vector_from_payload(payload): + if not isinstance(payload, dict): + return None + try: + return _vector( + float(payload.get("x", 0.0) or 0.0), + float(payload.get("y", 0.0) or 0.0), + float(payload.get("z", 0.0) or 0.0), + ) + except Exception: + return None + + +def _vector_from_json_property(obj, prop_name): + try: + payload = json.loads((getattr(obj, prop_name, "") or "").strip() or "{}") + except Exception: + return None + return _vector_from_payload(payload) + + +def _set_vector_json_property(obj, prop_name, vector, description): + if vector is None: + return + TerminalObjects.ensure_string_property( + obj, + prop_name, + "QET Assembly", + description, + json.dumps(_vector_payload(vector), ensure_ascii=False), + ) + + +def _mount_kind(obj): + carrier_kind = (getattr(obj, "QetCarrierKind", "") or "").strip() + if carrier_kind: + return carrier_kind + if (getattr(obj, "QetInstanceId", "") or "").strip(): + return "device" + try: + if TerminalObjects.is_terminal_object(obj): + return "terminal" + except Exception: + pass + text = "{0} {1}".format(getattr(obj, "Name", "") or "", getattr(obj, "Label", "") or "").lower() + if "wireduct" in text or "wire_duct" in text or "线槽" in text: + return "wire_duct" + if "rail" in text or "din" in text or "导轨" in text: + return "rail" + if "cabinet" in text or "panel" in text or "柜" in text or "安装板" in text: + return "cabinet" + return "object" + + +def _set_face_contact_mount_metadata(moving_obj, target_ref, moving_ref): + if moving_obj is None or not isinstance(target_ref, dict) or not isinstance(moving_ref, dict): + return + target_obj = target_ref.get("object") + if target_obj is None: + return + target_name = getattr(target_obj, "Name", "") or "" + target_label = getattr(target_obj, "Label", "") or target_name + target_base = _placement_base(target_obj) + moving_base = _placement_base(moving_obj) + values = { + "QetMountMode": "face_contact", + "QetMountKind": _mount_kind(moving_obj), + "QetMountHostName": target_name, + "QetMountHostLabel": target_label, + "QetMountHostKind": _mount_kind(target_obj), + "QetMountHostSubElement": target_ref.get("subelement_name", "") or "", + "QetMountContactSubElement": moving_ref.get("subelement_name", "") or "", + } + for prop_name, value in values.items(): + TerminalObjects.ensure_string_property( + moving_obj, + prop_name, + "QET Assembly", + "QET cabinet assembly mount metadata", + value, + ) + if target_base is not None: + _set_vector_json_property( + moving_obj, + "QetMountHostBaseJson", + target_base, + "QET cabinet assembly host base at bind time", + ) + if target_base is not None and moving_base is not None: + _set_vector_json_property( + moving_obj, + "QetMountLocalBaseJson", + _vector_sub(moving_base, target_base), + "QET cabinet assembly local base offset from host", + ) + + +def refresh_mount_hosted_objects(doc): + if doc is None: + raise ManualWiringPanelError("请先打开 QET 3D 工程文档。") + updated = [] + for obj in list(getattr(doc, "Objects", []) or []): + if (getattr(obj, "QetMountMode", "") or "").strip() != "face_contact": + continue + host_name = (getattr(obj, "QetMountHostName", "") or "").strip() + if not host_name: + continue + try: + host = doc.getObject(host_name) + except Exception: + host = None + if host is None: + continue + host_base = _placement_base(host) + local_base = _vector_from_json_property(obj, "QetMountLocalBaseJson") + if host_base is None or local_base is None: + continue + target_base = _vector_add(host_base, local_base) + current_base = _placement_base(obj) + changed = current_base is None or not _vectors_close(current_base, target_base) + if _set_placement_base(obj, target_base): + _set_vector_json_property( + obj, + "QetMountHostBaseJson", + host_base, + "QET cabinet assembly host base at last refresh", + ) + if changed: + updated.append(obj) + if updated: + try: + doc.recompute() + except Exception: + pass + return updated + + +def _select_object_for_followup_transform(obj): + if Gui is None or obj is None: + return + selection = getattr(Gui, "Selection", None) + if selection is None: + return + try: + if hasattr(selection, "clearSelection"): + selection.clearSelection() + if hasattr(selection, "addSelection"): + selection.addSelection(obj) + except Exception: + pass + + def _selected_point(): for picked in _selection_ex(): picked_points = list(getattr(picked, "PickedPoints", []) or []) @@ -575,6 +950,13 @@ def _selected_carrier_objects(): ] +def _selected_rail_object(): + for obj in _selected_carrier_objects(): + if (getattr(obj, "QetCarrierKind", "") or "").strip() == "rail": + return obj + return None + + def _existing_object_names(doc): return {getattr(obj, "Name", "") for obj in list(getattr(doc, "Objects", []) or [])} @@ -949,6 +1331,7 @@ class ManualWiringController: def __init__(self, terminal_exit_length=DEFAULT_TERMINAL_EXIT_LENGTH): self.terminal_exit_length = float(terminal_exit_length or 0.0) self.current_task = None + self.contact_target_ref = None self.start_terminal = None self.waypoints = [] self.preview_objects = [] @@ -1055,15 +1438,28 @@ class ManualWiringController: pass return updated + def set_contact_target_from_selection(self): + refs = _selected_contact_face_refs() + if len(refs) != 1: + raise ManualWiringPanelError("请只选择一个目标贴合面。") + self.contact_target_ref = refs[0] + return self.contact_target_ref + def align_selected_contact_faces(self): refs = _selected_contact_face_refs() - if len(refs) < 2: - raise ManualWiringPanelError("请先选择目标面,再按 Ctrl 选择要移动对象的接触面。") - if len(refs) > 2: - raise ManualWiringPanelError("只能选择两个面:第一个是目标面,第二个是要移动对象的接触面。") + if self.contact_target_ref is not None: + if len(refs) != 1: + raise ManualWiringPanelError("已设置目标面时,请只选择一个要移动对象的接触面。") + target = self.contact_target_ref + moving = refs[0] + else: + if len(refs) < 2: + raise ManualWiringPanelError("请先选择目标面,再按 Ctrl 选择要移动对象的接触面。") + if len(refs) > 2: + raise ManualWiringPanelError("只能选择两个面:第一个是目标面,第二个是要移动对象的接触面。") + target = refs[0] + moving = refs[1] - target = refs[0] - moving = refs[1] moving_object = _contact_transform_object(moving["object"]) if moving_object is None: raise ManualWiringPanelError("没有找到可移动的对象。") @@ -1078,11 +1474,13 @@ class ManualWiringController: if translation is None: raise ManualWiringPanelError("无法读取目标面的法向,不能执行贴合。") _translate_object(moving_object, translation) + _set_face_contact_mount_metadata(moving_object, target, moving) try: _active_document().recompute() except Exception: pass _activate_document(_active_document()) + _select_object_for_followup_transform(moving_object) return { "target_object": target["object"], "moving_object": moving_object, @@ -1093,6 +1491,53 @@ class ManualWiringController: "rotated": rotated, } + def refresh_mount_hosts(self): + return refresh_mount_hosted_objects(_active_document()) + + def create_terminal_block_from_selection( + self, + block_name="XT1", + count=10, + pitch_mm=5.2, + start_offset_mm=0.0, + model_path="", + ): + rail = _selected_rail_object() + if rail is None: + raise ManualWiringPanelError("请先选择一根已标记的导轨。") + return BatchAssembly.create_terminal_block( + _active_document(), + rail, + block_name=block_name, + count=count, + pitch_mm=pitch_mm, + start_offset_mm=start_offset_mm, + model_path=model_path, + ) + + def create_breakers_from_selection( + self, + base_name="QF", + count=3, + pitch_mm=18.0, + start_offset_mm=0.0, + terminal_numbers=("1", "2", "3", "4", "5", "6"), + model_path="", + ): + rail = _selected_rail_object() + if rail is None: + raise ManualWiringPanelError("请先选择一根已标记的导轨。") + return BatchAssembly.create_breakers( + _active_document(), + rail, + base_name=base_name, + count=count, + pitch_mm=pitch_mm, + start_offset_mm=start_offset_mm, + terminal_numbers=terminal_numbers, + model_path=model_path, + ) + def _clear_preview_objects(self): doc = getattr(App, "ActiveDocument", None) if doc is None: @@ -1383,7 +1828,11 @@ class ManualWiringTaskPanel: self.mark_duct_button = QtWidgets.QPushButton("标记为线槽") self.mark_cabinet_button = QtWidgets.QPushButton("标记为柜面") self.mark_rail_button = QtWidgets.QPushButton("标记为导轨") + self.batch_terminal_block_button = QtWidgets.QPushButton("批量端子排") + self.batch_breaker_button = QtWidgets.QPushButton("批量断路器") + self.set_contact_target_button = QtWidgets.QPushButton("设为贴合目标面") self.align_faces_button = QtWidgets.QPushButton("贴合到选中面") + self.refresh_mount_hosts_button = QtWidgets.QPushButton("刷新宿主装配") self.waypoint_button = QtWidgets.QPushButton("添加折点") self.delete_waypoint_button = QtWidgets.QPushButton("删除最后折点") self.end_button = QtWidgets.QPushButton("设为终点并生成") @@ -1416,7 +1865,13 @@ class ManualWiringTaskPanel: carrier_layout.addWidget(self.mark_cabinet_button) carrier_layout.addWidget(self.mark_rail_button) layout.addLayout(carrier_layout) + batch_layout = QtWidgets.QHBoxLayout() + batch_layout.addWidget(self.batch_terminal_block_button) + batch_layout.addWidget(self.batch_breaker_button) + layout.addLayout(batch_layout) + layout.addWidget(self.set_contact_target_button) layout.addWidget(self.align_faces_button) + layout.addWidget(self.refresh_mount_hosts_button) layout.addWidget(self.start_button) layout.addWidget(self.waypoint_button) layout.addWidget(self.delete_waypoint_button) @@ -1448,7 +1903,11 @@ class ManualWiringTaskPanel: self.mark_duct_button.clicked.connect(self.mark_wire_duct) self.mark_cabinet_button.clicked.connect(self.mark_cabinet) self.mark_rail_button.clicked.connect(self.mark_rail) + self.batch_terminal_block_button.clicked.connect(self.create_terminal_block) + self.batch_breaker_button.clicked.connect(self.create_breakers) + self.set_contact_target_button.clicked.connect(self.set_contact_target_face) self.align_faces_button.clicked.connect(self.align_selected_contact_faces) + self.refresh_mount_hosts_button.clicked.connect(self.refresh_mount_hosts) self.start_button.clicked.connect(self.set_start) self.waypoint_button.clicked.connect(self.add_waypoint) self.delete_waypoint_button.clicked.connect(self.delete_last_waypoint) @@ -1601,6 +2060,36 @@ class ManualWiringTaskPanel: except Exception as exc: self._set_error(str(exc)) + def create_terminal_block(self): + try: + options = _prompt_terminal_block_options(self.form) + if options is None: + return + report = self.controller.create_terminal_block_from_selection(**options) + self._set_status( + "已批量生成端子排:设备 {0} 个,工程端子 {1} 个。".format( + report.get("created_devices", 0), + report.get("created_terminals", 0), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def create_breakers(self): + try: + options = _prompt_breaker_options(self.form) + if options is None: + return + report = self.controller.create_breakers_from_selection(**options) + self._set_status( + "已批量生成小型断路器:设备 {0} 个,工程端子 {1} 个。".format( + report.get("created_devices", 0), + report.get("created_terminals", 0), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + def align_selected_contact_faces(self): try: result = self.controller.align_selected_contact_faces() @@ -1609,6 +2098,21 @@ class ManualWiringTaskPanel: except Exception as exc: self._set_error(str(exc)) + def set_contact_target_face(self): + try: + target = self.controller.set_contact_target_from_selection() + label = getattr(target.get("object"), "Label", "") or getattr(target.get("object"), "Name", "") + self._set_status("已设置贴合目标面:{0}".format(label or "选中面")) + except Exception as exc: + self._set_error(str(exc)) + + def refresh_mount_hosts(self): + try: + updated = self.controller.refresh_mount_hosts() + self._set_status("已刷新宿主装配:{0} 个对象。".format(len(updated))) + except Exception as exc: + self._set_error(str(exc)) + def set_start(self): try: terminal = self.controller.set_start_from_selection() diff --git a/src/Mod/FreeCADExchange/TemplateInstantiation.py b/src/Mod/FreeCADExchange/TemplateInstantiation.py index 1f9a59d..1252b61 100644 --- a/src/Mod/FreeCADExchange/TemplateInstantiation.py +++ b/src/Mod/FreeCADExchange/TemplateInstantiation.py @@ -207,7 +207,7 @@ def ensure_engineering_terminals_for_device(doc, device_group): slot_name=slot_name, ) try: - terminal_obj.ViewObject.Visibility = True + TerminalObjects.hide_engineering_terminal(terminal_obj) terminal_obj.ViewObject.ShapeColor = (0.0, 0.75, 1.0) except Exception: pass diff --git a/src/Mod/FreeCADExchange/TerminalObjects.py b/src/Mod/FreeCADExchange/TerminalObjects.py index 6aba212..f30d5f5 100644 --- a/src/Mod/FreeCADExchange/TerminalObjects.py +++ b/src/Mod/FreeCADExchange/TerminalObjects.py @@ -274,6 +274,17 @@ def is_template_terminal_object(obj): return is_terminal_hint_object(obj) and not is_terminal_object(obj) +def hide_engineering_terminal(obj): + try: + view_object = getattr(obj, "ViewObject", None) + if view_object is not None and hasattr(view_object, "Visibility"): + view_object.Visibility = False + return True + except Exception: + pass + return False + + def hide_template_terminal_hints(container): hidden = 0 if container is None: diff --git a/tests/python/freecad_exchange_batch_assembly_test.py b/tests/python/freecad_exchange_batch_assembly_test.py new file mode 100644 index 0000000..c5f8c07 --- /dev/null +++ b/tests/python/freecad_exchange_batch_assembly_test.py @@ -0,0 +1,260 @@ +import importlib +import sys +import tempfile +import types +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange" +if str(MODULE_DIR) not in sys.path: + sys.path.insert(0, str(MODULE_DIR)) + + +def _install_fake_freecad(): + class Vector: + def __init__(self, x=0.0, y=0.0, z=0.0): + self.x = float(x) + self.y = float(y) + self.z = float(z) + + class Rotation: + def multVec(self, vector): + return vector + + class Placement: + def __init__(self, base=None, rotation=None): + self.Base = base or Vector() + self.Rotation = rotation or Rotation() + + fake_freecad = types.ModuleType("FreeCAD") + fake_freecad.Vector = Vector + fake_freecad.Rotation = Rotation + fake_freecad.Placement = Placement + fake_freecad.ActiveDocument = None + fake_freecad.Console = types.SimpleNamespace( + PrintMessage=lambda *args, **kwargs: None, + PrintWarning=lambda *args, **kwargs: None, + PrintError=lambda *args, **kwargs: None, + ) + sys.modules["FreeCAD"] = fake_freecad + + fake_importgui = types.ModuleType("ImportGui") + + def insert(name, docName=None, merge=False, useLinkGroup=True): + doc = fake_freecad.ActiveDocument + obj = doc.addObject("Part::Feature", "ImportedBatchModel") + obj.ImportedPath = name + return obj + + fake_importgui.insert = insert + sys.modules["ImportGui"] = fake_importgui + + fake_freecadgui = types.ModuleType("FreeCADGui") + fake_freecadgui.addCommand = lambda *args, **kwargs: None + fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: []) + sys.modules["FreeCADGui"] = fake_freecadgui + + +class FakeViewObject: + def __init__(self): + self.Visibility = True + + +class FakeObject: + def __init__(self, name, type_id): + self.Name = name + self.Label = name + self.TypeId = type_id + self.PropertiesList = [] + self.Group = [] + self.InList = [] + self.ViewObject = FakeViewObject() + self.Placement = sys.modules["FreeCAD"].Placement() + self.Shape = None + + def isDerivedFrom(self, type_name): + if self.TypeId == type_name: + return True + if type_name == "App::DocumentObjectGroup": + return self.TypeId == "App::DocumentObjectGroup" + if type_name == "App::LocalCoordinateSystem": + return self.TypeId in {"Part::LocalCoordinateSystem", "PartDesign::CoordinateSystem"} + return False + + def addProperty(self, _prop_type, prop_name, _group_name, _description): + if prop_name not in self.PropertiesList: + self.PropertiesList.append(prop_name) + + def addObject(self, child): + if child not in self.Group: + self.Group.append(child) + if self not in child.InList: + child.InList.append(self) + + +class FakeDocument: + def __init__(self): + self.Objects = [] + self.Name = "QETScene" + self.recompute_count = 0 + + def addObject(self, type_name, name): + obj = FakeObject(name, type_name) + self.Objects.append(obj) + return obj + + def getObject(self, name): + for obj in self.Objects: + if obj.Name == name: + return obj + return None + + def recompute(self): + self.recompute_count += 1 + + +def _reload_modules(*extra_names): + for name in ["RoutingNetwork", "WiringObjects", "TemplateSemantics", "TerminalObjects", "BatchAssembly"] + list(extra_names): + sys.modules.pop(name, None) + terminal_objects = importlib.import_module("TerminalObjects") + batch_assembly = importlib.import_module("BatchAssembly") + return terminal_objects, batch_assembly + + +class BatchAssemblyTest(unittest.TestCase): + def test_create_terminal_block_places_slices_and_local_terminals_along_selected_rail(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + report = batch_assembly.create_terminal_block( + doc, + rail, + block_name="XT1", + count=3, + pitch_mm=5.2, + start_offset_mm=10.0, + ) + + self.assertEqual(3, report["created_devices"]) + self.assertEqual(3, report["created_terminals"]) + self.assertEqual("XT1", report["group"].Label) + placements = [item.Placement.Base.x for item in report["devices"]] + self.assertEqual([110.0, 115.2, 120.4], placements) + terminal_labels = [terminal.Label for terminal in report["terminals"]] + self.assertEqual(["XT1:1", "XT1:2", "XT1:3"], terminal_labels) + self.assertTrue(all(terminal.QetTerminalUuid.startswith("local:") for terminal in report["terminals"])) + self.assertTrue(all(not terminal.ViewObject.Visibility for terminal in report["terminals"])) + + def test_create_breakers_generates_numbered_devices_and_terminal_labels(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + report = batch_assembly.create_breakers( + doc, + rail, + base_name="QF", + count=2, + pitch_mm=18.0, + start_offset_mm=0.0, + terminal_numbers=("1", "2", "3", "4", "5", "6"), + ) + + self.assertEqual(["QF1", "QF2"], [device.Label for device in report["devices"]]) + self.assertEqual(12, report["created_terminals"]) + self.assertTrue(all(device.Name.startswith("QETDevice_") for device in report["devices"])) + self.assertEqual(["QF1", "QF2"], [device.QetInstanceId for device in report["devices"]]) + self.assertEqual(["QF1", "QF2"], [device.QetElementUuid for device in report["devices"]]) + labels = [terminal.Label for terminal in report["terminals"]] + self.assertEqual(["QF1:1", "QF1:2", "QF1:3", "QF1:4", "QF1:5", "QF1:6"], labels[:6]) + self.assertEqual(["QF2:1", "QF2:2", "QF2:3", "QF2:4", "QF2:5", "QF2:6"], labels[6:]) + self.assertEqual([0.0, 18.0], [device.Placement.Base.x for device in report["devices"]]) + + def test_created_breaker_local_slots_can_be_promoted_to_qet_terminal_uuid(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules("AutoRouting") + auto_routing = importlib.import_module("AutoRouting") + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + batch_assembly.create_breakers( + doc, + rail, + base_name="QF", + count=1, + terminal_numbers=("1", "2"), + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_uuid": "wire-1", + "start_instance_id": "QF1", + "start_terminal_uuid": "terminal-qf1-1", + "start_terminal_display": "1", + "end_instance_id": "QF1", + "end_terminal_uuid": "terminal-qf1-2", + "end_terminal_display": "2", + } + ], + } + + report = auto_routing.bind_wire_task_terminals_from_payload(doc, payload) + + self.assertEqual(2, report["bound"]) + indexed = auto_routing.index_terminals(doc) + self.assertIn("terminal-qf1-1", indexed) + self.assertIn("terminal-qf1-2", indexed) + self.assertFalse(any(key.startswith("local:QF1") for key in indexed)) + + def test_create_breakers_can_import_model_template_instead_of_placeholder_box(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + with tempfile.TemporaryDirectory() as temp_dir: + model_path = Path(temp_dir) / "breaker.step" + model_path.write_text("fake step", encoding="utf-8") + report = batch_assembly.create_breakers( + doc, + rail, + base_name="QF", + count=1, + model_path=str(model_path), + ) + + imported_children = [child for child in report["devices"][0].Group if getattr(child, "ImportedPath", "")] + self.assertEqual(1, len(imported_children)) + self.assertEqual(str(model_path), imported_children[0].QetBatchSourceModelPath) + self.assertEqual("QF1 模型", imported_children[0].Label) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/python/freecad_exchange_manual_wiring_panel_test.py b/tests/python/freecad_exchange_manual_wiring_panel_test.py index 3c1e003..1d104fc 100644 --- a/tests/python/freecad_exchange_manual_wiring_panel_test.py +++ b/tests/python/freecad_exchange_manual_wiring_panel_test.py @@ -61,9 +61,17 @@ def _install_fake_freecad(): fake_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.addCommand = lambda *args, **kwargs: None fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None + def clear_selection(): + selection_state["selection"] = [] + + def add_selection(obj): + selection_state["selection"] = [obj] + fake_freecadgui.Selection = types.SimpleNamespace( getSelection=lambda: list(selection_state["selection"]), getSelectionEx=lambda: list(selection_state["selection_ex"]), + clearSelection=clear_selection, + addSelection=add_selection, ) fake_freecadgui.Control = types.SimpleNamespace( activeDialog=lambda: False, @@ -190,6 +198,7 @@ def _reload_modules(): "ManualWiring", "TemplateAuthoring", "ExchangeWriteBack", + "BatchAssembly", "ManualWiringPanel", ]: sys.modules.pop(name, None) @@ -222,6 +231,122 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual(str(asset), resolved) + def test_controller_batch_creates_terminal_block_from_selected_rail(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(50, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + selection_state["selection"] = [rail] + + report = panel.ManualWiringController().create_terminal_block_from_selection( + block_name="XT1", + count=2, + pitch_mm=5.2, + ) + + self.assertEqual(2, report["created_devices"]) + self.assertEqual(["XT1:1", "XT1:2"], [terminal.Label for terminal in report["terminals"]]) + + def test_controller_batch_breaker_default_creates_three_pole_terminal_numbers(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + selection_state["selection"] = [rail] + + report = panel.ManualWiringController().create_breakers_from_selection( + base_name="QF", + count=1, + ) + + self.assertEqual(1, report["created_devices"]) + self.assertEqual(["QF1:1", "QF1:2", "QF1:3", "QF1:4", "QF1:5", "QF1:6"], [terminal.Label for terminal in report["terminals"]]) + + def test_controller_batch_accepts_empty_model_path_from_dialog_options(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + selection_state["selection"] = [rail] + + report = panel.ManualWiringController().create_breakers_from_selection( + base_name="QF", + count=1, + model_path="", + ) + + self.assertEqual(1, report["created_devices"]) + + def test_batch_terminal_block_options_validate_user_fields(self): + _install_fake_freecad() + _terminal_objects, panel = _reload_modules() + + options = panel._batch_terminal_block_options( + block_name=" XT2 ", + count=12, + pitch_mm=6.2, + start_offset_mm=-5, + ) + + self.assertEqual( + { + "block_name": "XT2", + "count": 12, + "pitch_mm": 6.2, + "start_offset_mm": -5.0, + }, + options, + ) + + def test_batch_breaker_options_parse_terminal_number_text(self): + _install_fake_freecad() + _terminal_objects, panel = _reload_modules() + + options = panel._batch_breaker_options( + base_name=" QF ", + count=2, + pitch_mm=18, + start_offset_mm=0, + terminal_numbers_text="1,2,3 4;5;6", + ) + + self.assertEqual(("1", "2", "3", "4", "5", "6"), options["terminal_numbers"]) + self.assertNotIn("model_path", options) + + def test_batch_breaker_options_reject_duplicate_terminal_numbers(self): + _install_fake_freecad() + _terminal_objects, panel = _reload_modules() + + with self.assertRaises(panel.ManualWiringPanelError): + panel._batch_breaker_options( + base_name="QF", + count=1, + pitch_mm=18, + start_offset_mm=0, + terminal_numbers_text="1,2,1", + ) + def test_controller_rejects_local_terminal_as_manual_wiring_start(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() @@ -540,6 +665,269 @@ class ManualWiringPanelTest(unittest.TestCase): ) self.assertEqual("normal", result["translation_mode"]) + def test_controller_records_face_contact_mount_host_after_alignment(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + terminal_objects.ensure_string_property( + cabinet, + "QetCarrierKind", + "QET Wiring", + "3D wiring carrier kind", + "cabinet", + ) + terminal_objects.ensure_string_property( + rail, + "QetCarrierKind", + "QET Wiring", + "3D wiring carrier kind", + "rail", + ) + + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(5, 6, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=rail, + ), + ] + + result = panel.ManualWiringController().align_selected_contact_faces() + + self.assertIs(cabinet, result["target_object"]) + self.assertEqual("face_contact", rail.QetMountMode) + self.assertEqual("CabinetPanel", rail.QetMountHostName) + self.assertEqual("CabinetPanel", rail.QetMountHostLabel) + self.assertEqual("cabinet", rail.QetMountHostKind) + self.assertEqual("rail", rail.QetMountKind) + self.assertEqual("Face1", rail.QetMountHostSubElement) + self.assertEqual("Face2", rail.QetMountContactSubElement) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, json.loads(rail.QetMountHostBaseJson)) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountLocalBaseJson)) + + def test_refresh_mount_hosted_objects_moves_child_by_host_delta(self): + _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + cabinet.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) + rail.Placement = app.Placement(app.Vector(120, 0, 5), app.Rotation()) + terminal_objects.ensure_string_property( + rail, + "QetMountMode", + "QET Assembly", + "QET cabinet assembly mount metadata", + "face_contact", + ) + terminal_objects.ensure_string_property( + rail, + "QetMountHostName", + "QET Assembly", + "QET cabinet assembly mount metadata", + "CabinetPanel", + ) + terminal_objects.ensure_string_property( + rail, + "QetMountLocalBaseJson", + "QET Assembly", + "QET cabinet assembly local base offset", + json.dumps({"x": 20.0, "y": 0.0, "z": 5.0}, ensure_ascii=False), + ) + cabinet.Placement = app.Placement(app.Vector(130, 0, 0), app.Rotation()) + + updated = panel.refresh_mount_hosted_objects(doc) + + self.assertEqual([rail], updated) + self.assertEqual((150.0, 0.0, 5.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) + self.assertEqual({"x": 130.0, "y": 0.0, "z": 0.0}, json.loads(rail.QetMountHostBaseJson)) + + def test_controller_refreshes_mount_hosted_objects_from_active_document(self): + _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + cabinet.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation()) + rail.Placement = app.Placement(app.Vector(15, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property( + rail, + "QetMountMode", + "QET Assembly", + "QET cabinet assembly mount metadata", + "face_contact", + ) + terminal_objects.ensure_string_property( + rail, + "QetMountHostName", + "QET Assembly", + "QET cabinet assembly mount metadata", + "CabinetPanel", + ) + terminal_objects.ensure_string_property( + rail, + "QetMountLocalBaseJson", + "QET Assembly", + "QET cabinet assembly local base offset", + json.dumps({"x": 5.0, "y": 0.0, "z": 0.0}, ensure_ascii=False), + ) + cabinet.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) + + updated = panel.ManualWiringController().refresh_mount_hosts() + + self.assertEqual([rail], updated) + self.assertEqual((25.0, 0.0, 0.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) + + def test_controller_uses_stored_target_face_for_single_moving_face_alignment(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + controller = panel.ManualWiringController() + + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ) + ] + controller.set_contact_target_from_selection() + + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(5, 6, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=rail, + ) + ] + result = controller.align_selected_contact_faces() + + self.assertIs(rail, result["moving_object"]) + self.assertEqual((0.0, 0.0, 1.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) + + def test_controller_uses_largest_object_face_when_no_subface_is_selected(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + small_face = types.SimpleNamespace( + ShapeType="Face", + Area=10.0, + CenterOfMass=app.Vector(0, 0, 5), + normalAt=lambda u, v: app.Vector(0, 1, 0), + ) + large_face = types.SimpleNamespace( + ShapeType="Face", + Area=1000.0, + CenterOfMass=app.Vector(0, 0, 0), + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + cabinet.Shape = types.SimpleNamespace(Faces=[small_face, large_face]) + selection_state["selection"] = [cabinet] + selection_state["selection_ex"] = [] + + target = panel.ManualWiringController().set_contact_target_from_selection() + + self.assertIs(large_face, target["face"]) + self.assertEqual((0.0, 0.0, 0.0), (target["point"].x, target["point"].y, target["point"].z)) + + def test_controller_moves_qet_device_root_when_selected_face_belongs_to_child_shape(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + root = terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") + terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-a") + child = doc.addObject("Part::Feature", "DeviceSolid") + device.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + child.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + root.addObject(device) + device.addObject(child) + + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=child, + ), + ] + + result = panel.ManualWiringController().align_selected_contact_faces() + + self.assertIs(device, result["moving_object"]) + self.assertEqual((0.0, 0.0, 1.0), (device.Placement.Base.x, device.Placement.Base.y, device.Placement.Base.z)) + self.assertEqual((0.0, 0.0, 0.0), (child.Placement.Base.x, child.Placement.Base.y, child.Placement.Base.z)) + self.assertEqual([device], selection_state["selection"]) + def test_controller_requires_two_faces_for_contact_alignment(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() diff --git a/tests/python/freecad_exchange_template_instantiation_test.py b/tests/python/freecad_exchange_template_instantiation_test.py index fa78025..1ed0231 100644 --- a/tests/python/freecad_exchange_template_instantiation_test.py +++ b/tests/python/freecad_exchange_template_instantiation_test.py @@ -211,6 +211,7 @@ class TemplateInstantiationTest(unittest.TestCase): self.assertEqual(1, len(terminals)) self.assertEqual("terminal-p1", terminals[0].QetTerminalUuid) self.assertEqual("qet", terminals[0].QetTerminalBindingMode) + self.assertFalse(terminals[0].ViewObject.Visibility) self.assertFalse(p1.ViewObject.Visibility) def test_device_without_template_slots_reports_no_created_terminals(self): From 35218b85bcf33552540c3c60b8898f6999bf428f Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Wed, 3 Jun 2026 11:31:08 +0800 Subject: [PATCH 56/63] =?UTF-8?q?feat(freecad):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84=E8=AF=8A?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 31 +- src/Mod/FreeCADExchange/AutoRouting.py | 153 ++++++++- src/Mod/FreeCADExchange/RoutingNetwork.py | 226 ++++++++++++- .../freecad_exchange_auto_routing_test.py | 319 ++++++++++++++++++ 4 files changed, 704 insertions(+), 25 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 982d210..808b7e1 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -260,6 +260,8 @@ FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 +路径网络检查会诊断异常长的 `TerminalAccess`。当端子接入段明显过长时,报告会提示“端子接入过长”,建议补设备局部路径、移动设备,或补一段 `UserPath` / 线槽靠近端子。这类诊断用于避免设备未摆放好时生成看起来悬空或穿越设备区域的接入线。 + 面板还提供“并行线间距 mm”和“并行线方向”,用于控制多根导线共用同一路径时的可视 lane 偏移。方向默认 `auto`,也可以手动指定 `x`、`y`、`z`。这些设置只影响 3D 显示上导线之间的错位方式,不代表真实线槽截面内的排布位置。 ### 2.1 路由优先级 @@ -268,7 +270,7 @@ FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框 1. `WireDuct`:线槽中心路径,最高优先级。 2. `RoutingPath`:历史兼容和内部调试用的明确路由线,不作为当前正式入口。 -3. `TerminalAccess`:端子到路由网络的自动接入路径,只用于把工程端子接入线槽/布线面。 +3. `TerminalAccess`:端子到路由网络的自动接入路径,只用于把工程端子接入 `WireDuct` / `UserPath` / `WiringCutOut` / `RoutingRange` 等主路径网络。 4. `AuxiliaryPath`:辅助路径,后续扩展使用。 5. `RoutingRange`:柜面/安装板等支撑面生成的辅助路由区域,成本较高,只用于过渡或没有线槽时兜底。 6. `WireDuctOpenEnd`:线槽开口端横向路径,用于模拟 EPLAN 在线槽开口端生成的横向 routing path。 @@ -314,7 +316,7 @@ QetTerminalBindingMode = qet ```text QetRoutingRole = "RoutingCarrier" -QetRouteCarrierKind = "WireDuct" | "RoutingPath" | "AuxiliaryPath" | "RoutingRange" +QetRouteCarrierKind = "WireDuct" | "WireDuctOpenEnd" | "WiringCutOut" | "UserPath" | "RoutingPath" | "AuxiliaryPath" | "RoutingRange" | "TerminalAccess" CanRouteWire = true QetProjectUuid = Points = [Vector, Vector, ...] @@ -325,7 +327,7 @@ Points = [Vector, Vector, ...] ```text QetRouteSourceName = QetRouteSourceLabel = -QetRouteSourceKind = "WireDuct" | "RoutingRange" | "WiringCutOut" +QetRouteSourceKind = "WireDuct" | "RoutingRange" | "WiringCutOut" | "UserPath" | "TerminalAccess" ``` 这些属性只用于 FreeCAD 文档内部刷新和清理,不写入数据库,也不要求 QET 提供。 @@ -543,6 +545,16 @@ QetWiringCutOutBridgeExtensionMm = 20.0 批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻线槽自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 +一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长等问题带出来。 + +当单条路线使用 `RoutingRange` 或 `AuxiliaryPath` 时,批量报告会提示“路径质量提示”,说明该导线可能没有完全优先进入线槽。这个提示不阻止布线,只用于暴露“当前路径依赖布线面兜底”的情况,方便后续补线槽、补 `UserPath` 或调整设备位置。批量诊断 JSON 也会记录这类提示:`route_quality_warning_count` 表示依赖布线面/辅助路径的导线数量,`route_quality_warning_samples` 保留少量导线样例及其使用的 carrier 类型。 + +路径网络检查还会识别“只有 `RoutingRange`、没有 `WireDuct` / `UserPath` / `WiringCutOut` 主路径”的情况,并记录 `routing_range_only_network`。这类网络可以作为无线槽或路径不完整时的临时兜底,但不是推荐的第一版主路径形态;手动测试看到该提示时,优先补线槽、补 `UserPath` 或补过线孔路径。 + +如果当前文档没有任何可用路径段,路径网络检查会记录 `empty_routing_path_network`,中文报告显示“布线路径网络为空”。这表示还没有生成可供自动布线搜索的线槽、`UserPath`、过线孔或布线面路径,不能把 0 carrier / 0 segment 当作检查通过。 + +如果 carrier 对象存在但 `Points` 为空、只有一个点,或多个点归一化后仍不足两个有效点,路径网络检查会记录 `invalid_route_carriers`,中文报告提示“路径对象几何无效”。这通常意味着用户路径、线槽路径或刷新后的 carrier 几何已经损坏,需要重新生成该路径对象。 + 如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号和 lane 间距。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。 当单条路线的最大并行线数超过该路线 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。 @@ -613,7 +625,7 @@ tests/python/freecad_exchange_auto_routing_test.py 16. 无线槽或线槽不完整时,可使用自动识别的支撑面辅助路径完成贴面布线。 17. 面板流程已简化为“准备布线布局空间 -> 生成布线路径网络 -> 生成布线连接”。 18. “准备布线布局空间”始终按整份文档识别线槽、支撑面和工程端子,并标记障碍处理方式。 -19. “生成布线路径网络”按 EPLAN 的 Generate routing path network 语义生成 WireDuct、RoutingRange 和 TerminalAccess carrier;有选择时,选中线槽只作为额外识别提示,仍会扫描整份文档。 +19. “生成布线路径网络”按 EPLAN 的 Generate routing path network 语义生成 `WireDuct` / `UserPath` / `WiringCutOut` / `RoutingRange` / `TerminalAccess` carrier;有选择时,选中线槽或草图路径只作为额外识别提示,仍会扫描整份文档。 20. “生成布线连接”会先更新同一套布线路径网络,再按全部 QET 导线任务批量求路。 21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 @@ -715,6 +727,7 @@ end_terminal_display 3. 系统把选中路径转换为 `UserPath` carrier,并参与后续自动布线最短路搜索。“选中路径作为用户路径”只创建用户路径;“生成布线路径网络”会同时更新线槽、布线面、端子接入等完整网络。 4. 再次选择同一个路径对象生成网络时,系统会刷新原 carrier,不会重复生成。 5. 如果删除了原草图/线段源对象,再点击“选中路径作为用户路径”或重新生成网络,系统会清理对应的失效 `UserPath` carrier。 +6. 如果源对象设置了 `QetRouteCarrierCapacity` 或 `QetWireCapacity`,生成/刷新出的 `UserPath` 会继承该容量,用于多根线共路和容量提示。 `UserPath` 与线槽的关系: @@ -739,6 +752,8 @@ QetTerminalLocalRoutePointsJson 自动生成 `TerminalAccess` 时,系统会先把这些局部点按端子和父设备的 `Placement` 转成全局点,再从局部路径末端连接到最近的柜内主路径、线槽、用户路径或布线面。没有该字段时,仍使用原来的端子 LCS `+Z` 方向短出线。 +路径网络检查也使用同一口径:如果端子有有效局部路径,端子到主路径网络的接入距离按局部路径末端计算,而不是按默认 LCS 出线点计算。这样可以避免局部路径已经接入线槽、但诊断仍误报“端子未接入”的情况。 + 当前模板链路已经支持把局部路径从设备模板带到工程端子。模板 sidecar 或 `QetTemplateSlotsJson` 可以在对应端子槽位上提供: ```json @@ -756,6 +771,8 @@ QetTerminalLocalRoutePointsJson 导入/更新工程端子时,FreeCAD 会把 `local_route_points` 写入该端子的 `QetTerminalLocalRoutePointsJson`。后续自动生成 `TerminalAccess` 和最终导线几何时都会使用这段局部路径。 +路径网络检查会校验端子局部路径元数据。`QetTerminalLocalRoutePointsJson` / `QetLocalRoutePointsJson` 必须是 JSON 数组,并且至少能解析出两个不同的有效点;如果 JSON 格式错误、不是数组或有效点不足,诊断对象会记录 `invalid_terminal_local_routes`,中文报告会提示“端子局部路径无效”。这类问题不会让 FreeCAD 依赖 QET 提供 3D 路径,只是提示模板端子或工程端子的 3D 局部出线元数据需要修正。 + 如果直接在 FCStd 模板端子 LCS 上维护,也可以给模板端子写入同名属性 `QetTerminalLocalRoutePointsJson`。当前模板作者工具提供了内部函数: ```python @@ -872,9 +889,9 @@ PE 线优先路径 当前版本验收只看“能否生成布线连接”: 1. 文档中有至少两个真实工程端子。 -2. 文档中有至少一条 `WireDuct` carrier,或有可作为低优先级路径的 `RoutingRange` 支撑面 carrier。 -3. 执行“生成布线网络路径”后,能生成 `WireDuct` carrier。 -4. 执行“生成布线布局空间”后,能生成或复用 `WireDuct` / `RoutingRange` carrier,并为工程端子生成 `TerminalAccess` 接入 carrier。 +2. 文档中有至少一条 `WireDuct` / `UserPath` / `WiringCutOut` carrier,或有可作为低优先级路径的 `RoutingRange` 支撑面 carrier。 +3. 执行“生成布线路径网络”后,能生成或复用 `WireDuct` / `UserPath` / `WiringCutOut` / `RoutingRange` carrier。 +4. 执行“生成布线布局空间”后,能识别线槽、用户路径、穿线孔或支撑面语义,并为工程端子生成 `TerminalAccess` 接入 carrier。 5. 存在导线任务时执行“生成布线连接”,会先准备布线路径网络,再批量生成 `AutoSuggested` 导线。 6. 生成导线在 `QETWiring_04_Routed` 下可见。 7. 没有路由网络时正式布线不生成长距离悬空线。 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index e697d2c..ae356d9 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -1912,21 +1912,14 @@ def _route_track_carrier_kinds(route_track): def _route_quality_warning_summary(report): warning_count = 0 sample = None - for route in report.get("routes", []) or []: - if not isinstance(route, dict): - continue - carrier_kinds = _route_track_carrier_kinds(route.get("route_track", {})) - warning_labels = [ - label - for kind, label in _ROUTE_QUALITY_WARNING_KIND_LABELS.items() - if carrier_kinds.get(kind, 0) - ] + for warning in _route_quality_warning_samples(report, limit=0): + warning_labels = list(warning.get("carrier_labels", []) or []) if not warning_labels: continue warning_count += 1 if sample is None: sample = { - "wire": _wire_sample_text(route), + "wire": warning.get("wire_label") or warning.get("wire_uuid") or "未知导线", "labels": warning_labels, } if warning_count <= 0: @@ -1937,6 +1930,37 @@ def _route_quality_warning_summary(report): } +def _route_quality_warning_samples(report, limit=8): + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + carrier_kinds = _route_track_carrier_kinds(route.get("route_track", {})) + warning_kinds = [ + kind + for kind in _ROUTE_QUALITY_WARNING_KIND_LABELS + if carrier_kinds.get(kind, 0) + ] + if not warning_kinds: + continue + if max_samples <= 0 or len(samples) < max_samples: + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "carrier_kinds": warning_kinds, + "carrier_labels": [ + _ROUTE_QUALITY_WARNING_KIND_LABELS.get(kind, kind) + for kind in warning_kinds + ], + } + ) + return samples + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -1984,6 +2008,13 @@ def format_eplan_connection_route_report(report): prepared_layout.get("surface_carriers", 0), prepared_layout.get("terminal_access_carriers", 0), ) + 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 = [ + _routing_path_network_issue_label(code) + for code in list(path_diagnostic.get("issue_codes", []) or [])[:3] + ] + message += "\n路径网络检查提示:{0}。".format("、".join(issue_labels) if issue_labels else "存在问题") if report.get("skipped_missing_route_network", 0) > 0: message += "\n缺少布线路径网络:{0} 条导线已跳过。请先生成线槽、布线面或布线路径网络。".format( report.get("skipped_missing_route_network", 0) @@ -2202,6 +2233,9 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): payload["route_count"] = len(routes) payload["route_samples"] = [_compact_route_sample(route) for route in routes[:limit]] payload["route_sample_count"] = len(payload["route_samples"]) + 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 payload["diagnostic_payload"] = "compact-routing-connection-batch-v1" return payload @@ -2353,6 +2387,42 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None): } +def _compact_routing_path_network_diagnostic(diagnostic): + if not isinstance(diagnostic, dict): + return {} + issues = _dict_items(diagnostic.get("issues", []) or []) + return { + "ok": bool(diagnostic.get("ok", False)), + "issue_count": len(issues), + "issue_codes": [str(issue.get("code", "") or "") for issue in issues if issue.get("code", "")], + "issues": [ + { + "severity": issue.get("severity", ""), + "code": issue.get("code", ""), + "count": issue.get("count", 0), + } + for issue in issues + ], + "summary": diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {}, + } + + +_PATH_NETWORK_ISSUE_LABELS = { + "empty_routing_path_network": "布线路径网络为空", + "invalid_route_carriers": "路径对象几何无效", + "routing_range_only_network": "仅使用布线面兜底", + "invalid_terminal_local_routes": "端子局部路径无效", + "long_terminal_accesses": "端子接入过长", + "unconnected_terminals": "端子未接入", + "wire_duct_endpoint_breaks": "线槽端点疑似断开", + "isolated_network_components": "存在孤立路径网络", +} + + +def _routing_path_network_issue_label(code): + return _PATH_NETWORK_ISSUE_LABELS.get(str(code or ""), str(code or "未知问题")) + + def _format_distance_mm(value): try: return "{0:.1f} mm".format(float(value)) @@ -2409,6 +2479,10 @@ def format_routing_path_network_report(diagnostic): return message message = "布线路径网络检查发现 {0} 类问题。".format(len(issues)) + empty_network = any(issue.get("code") == "empty_routing_path_network" for issue in issues) + if empty_network: + message += "\n布线路径网络为空:没有可用路径段。请先生成线槽、UserPath、过线孔或布线面路径。" + unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or []) if unconnected: sample = unconnected[0] @@ -2428,6 +2502,37 @@ def format_routing_path_network_report(diagnostic): _format_point_text(sample.get("point")), ) + invalid_carriers = _dict_items(diagnostic.get("invalid_route_carriers", []) or []) + if invalid_carriers: + sample = invalid_carriers[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + carrier_text = carrier.get("label") or carrier.get("name") or "未知路径对象" + message += "\n路径对象几何无效:{0},有效点不足。请重新生成该 UserPath/线槽路径或检查 Points。".format( + carrier_text + ) + + long_accesses = _dict_items(diagnostic.get("long_terminal_accesses", []) or []) + if long_accesses: + sample = long_accesses[0] + message += "\n端子接入过长:{0},接入段 {1},建议补设备局部路径、移动设备或补一段 UserPath/线槽靠近端子。".format( + _diagnostic_terminal_text(sample), + _format_distance_mm(sample.get("terminal_access_length_mm")), + ) + + invalid_local_routes = _dict_items(diagnostic.get("invalid_terminal_local_routes", []) or []) + if invalid_local_routes: + sample = invalid_local_routes[0] + message += "\n端子局部路径无效:{0},字段 {1}。请检查模板端子局部路径或 QetTerminalLocalRoutePointsJson。".format( + _diagnostic_terminal_text(sample), + sample.get("property_name", "未知字段"), + ) + + routing_range_only = diagnostic.get("routing_range_only_network", {}) + if isinstance(routing_range_only, dict) and routing_range_only: + message += "\n当前路径网络仅使用布线面兜底:RoutingRange {0} 条,主路径 0 条。建议补线槽、UserPath 或过线孔作为柜内主路径。".format( + int(routing_range_only.get("routing_range_carriers", 0) or 0) + ) + isolated = _dict_items(diagnostic.get("isolated_components", []) or []) if isolated: sample = isolated[0] @@ -2435,7 +2540,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 (unconnected or possible_breaks or isolated): + if not (empty_network or unconnected or possible_breaks or invalid_carriers or long_accesses or invalid_local_routes or routing_range_only or isolated): first_issue = issues[0] message += "\n首个问题:{0} ({1})。".format( first_issue.get("code", "unknown"), @@ -2475,6 +2580,31 @@ def route_eplan_connections( options=opts, selection_ex=selection_ex, ) + routing_path_network_diagnostic = {} + 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), + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + ) + except Exception as exc: + routing_path_network_diagnostic = { + "ok": False, + "issue_count": 1, + "issue_codes": ["routing_path_network_diagnostic_error"], + "issues": [ + { + "severity": "error", + "code": "routing_path_network_diagnostic_error", + "count": 1, + } + ], + "summary": {}, + "error": str(exc), + } target_payload = payload if target_payload is None: @@ -2496,6 +2626,7 @@ def route_eplan_connections( report["routing_method"] = "eplan-route-v1" report["routing_path_network_updated"] = bool(update_network) + report["routing_path_network_diagnostic"] = routing_path_network_diagnostic 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/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index fefcf2e..de8bb26 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -39,6 +39,7 @@ DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH = 20.0 DEFAULT_ROUTE_PATH_FACE_OFFSET = 2.0 DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT = 2.5 DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE = 1000.0 +DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE = 500.0 DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0 DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 WIRE_DUCT_OBSTACLE_MODE = "PassThrough" @@ -1211,6 +1212,10 @@ def _points_from_selection_item(selection_item): points.append(center) obj = getattr(selection_item, "Object", None) + if obj is not None and _is_route_path_source_object(obj): + for point in list(getattr(obj, "Points", []) or []): + points.append(_vector(point)) + shape = getattr(obj, "Shape", None) if shape is not None and _is_route_path_source_object(obj): for edge in list(getattr(shape, "Edges", []) or []): @@ -1469,6 +1474,8 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") if id(source) in seen_sources: continue seen_sources.add(id(source)) + if is_route_carrier(source): + continue if ( _is_wire_duct_candidate(source) or _is_support_surface_candidate(source) @@ -1482,10 +1489,12 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") points = _project_points_to_face(points, support_face) label = "QET User Route Path {0}".format(index) + capacity = 1 if source is not None: label = "QET User Route Path {0}".format( getattr(source, "Label", "") or getattr(source, "Name", "") or index ) + capacity = _route_carrier_capacity_value(source, default=1) live_carrier = _live_source_carrier(doc, source) if live_carrier is not None: if _update_route_carrier( @@ -1493,6 +1502,7 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") points, project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, ): _mark_user_path_source(source, live_carrier) continue @@ -1503,6 +1513,7 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") label=label, project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, ) if source is not None: _mark_user_path_source(source, carrier) @@ -2245,6 +2256,58 @@ def _terminal_local_route_points(terminal): return [] +def _terminal_local_route_issue(terminal): + invalid_samples = [] + saw_raw = False + for property_name in ("QetTerminalLocalRoutePointsJson", "QetLocalRoutePointsJson"): + raw = (getattr(terminal, property_name, "") or "").strip() + if not raw: + continue + saw_raw = True + try: + parsed = json.loads(raw) + except Exception as exc: + invalid_samples.append( + { + "property_name": property_name, + "reason": "invalid_json", + "message": str(exc), + "raw_sample": raw[:160], + } + ) + continue + if not isinstance(parsed, list): + invalid_samples.append( + { + "property_name": property_name, + "reason": "not_array", + "message": "Local route points JSON must be an array.", + "raw_sample": raw[:160], + } + ) + continue + points = [_json_route_point(item) for item in parsed if item is not None] + valid_points = [point for point in points if point is not None] + if len(_normalized_route_points(valid_points)) >= 2: + return None + invalid_samples.append( + { + "property_name": property_name, + "reason": "too_few_valid_points", + "message": "Local route points must contain at least two distinct valid points.", + "raw_sample": raw[:160], + "valid_point_count": len(valid_points), + } + ) + 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_local_route_invalid" + return payload + + def _terminal_parent_chain(terminal): chain = [] current = terminal @@ -3070,6 +3133,35 @@ def _network_summary_from_graph(network): } +def _routing_range_only_network_payload(summary): + if not isinstance(summary, dict): + return {} + kinds = summary.get("kinds", {}) + if not isinstance(kinds, dict): + return {} + primary_route_carriers = sum( + int(kinds.get(kind, 0) or 0) + for kind in ( + ROUTE_CARRIER_KIND_WIRE_DUCT, + ROUTE_CARRIER_KIND_USER_PATH, + ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ) + ) + routing_range_carriers = int(kinds.get(ROUTE_CARRIER_KIND_ROUTING_RANGE, 0) or 0) + if routing_range_carriers <= 0 or primary_route_carriers > 0: + return {} + return { + "primary_route_carriers": primary_route_carriers, + "routing_range_carriers": routing_range_carriers, + "primary_route_kinds": [ + ROUTE_CARRIER_KIND_WIRE_DUCT, + ROUTE_CARRIER_KIND_USER_PATH, + ROUTE_CARRIER_KIND_WIRING_CUT_OUT, + ], + "fallback_kind": ROUTE_CARRIER_KIND_ROUTING_RANGE, + } + + def _route_graph_components(network): nodes = network.get("nodes", {}) or {} edges = network.get("edges", {}) or {} @@ -3144,6 +3236,35 @@ def _wire_duct_endpoint_breaks(network): return breaks +def _invalid_route_carriers(network): + invalid = [] + for carrier in network.get("carriers", []) or []: + points = _carrier_points(carrier) + normalized = _normalized_route_points(points) + if len(normalized) >= 2: + continue + invalid.append( + { + "carrier": _carrier_track_payload(carrier), + "point_count": len(points), + "distinct_point_count": len(normalized), + "code": "route_carrier_invalid_geometry", + } + ) + return invalid + + +def _polyline_length(points): + total = 0.0 + previous = None + for point in points or []: + current = _vector(point) + if previous is not None: + total += _distance(previous, current) + previous = current + return total + + def _terminal_diagnostic_payload(terminal): return { "name": getattr(terminal, "Name", ""), @@ -3168,32 +3289,66 @@ def diagnose_routing_path_network( summary = _network_summary_from_graph(network) isolated_components = components if len(components) > 1 else [] unconnected_terminals = [] + long_terminal_accesses = [] + invalid_terminal_local_routes = [] + routing_range_only_network = _routing_range_only_network_payload(summary) max_distance = max(float(terminal_access_max_distance or 0.0), 0.0) + 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 _collect_routable_terminals(doc): - exit_point = _terminal_exit_point(terminal, terminal_exit_length) + 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) + 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) access_live = access_carrier is not None and is_route_carrier(access_carrier) too_far = nearest_point is None or (max_distance > 0.0 and float(distance or 0.0) > max_distance) connected_directly = nearest_point is not None and float(distance or 0.0) <= DEFAULT_NODE_TOLERANCE - if (access_live or connected_directly) and not too_far: + if not ((access_live or connected_directly) and not too_far): + payload = _terminal_diagnostic_payload(terminal) + payload.update( + { + "access_carrier": getattr(access_carrier, "Name", "") if access_carrier is not None else "", + "nearest_network_distance_mm": None if distance is None else float(distance), + "nearest_network_point": None if nearest_point is None else _point_payload(nearest_point), + "terminal_access_max_distance_mm": float(max_distance), + "terminal_exit_length_mm": float(max(float(terminal_exit_length or 0.0), 0.0)), + "code": "terminal_access_missing" if not access_live else "terminal_access_too_far", + } + ) + unconnected_terminals.append(payload) + continue + + access_points = _carrier_points(access_carrier) if access_live else [] + access_length = _polyline_length(access_points) + if access_length <= warning_distance: continue payload = _terminal_diagnostic_payload(terminal) payload.update( { "access_carrier": getattr(access_carrier, "Name", "") if access_carrier is not None else "", - "nearest_network_distance_mm": None if distance is None else float(distance), - "nearest_network_point": None if nearest_point is None else _point_payload(nearest_point), + "terminal_access_length_mm": float(access_length), + "terminal_access_warning_distance_mm": float(warning_distance), "terminal_access_max_distance_mm": float(max_distance), - "terminal_exit_length_mm": float(max(float(terminal_exit_length or 0.0), 0.0)), - "code": "terminal_access_missing" if not access_live else "terminal_access_too_far", + "code": "terminal_access_long", } ) - unconnected_terminals.append(payload) + long_terminal_accesses.append(payload) possible_breaks = _wire_duct_endpoint_breaks(network) + invalid_route_carriers = _invalid_route_carriers(network) issues = [] + if int(summary.get("segments", 0) or 0) <= 0: + issues.append( + { + "severity": "error", + "code": "empty_routing_path_network", + "message": "Routing path network has no usable segments.", + "count": 0, + } + ) if isolated_components: issues.append( { @@ -3221,6 +3376,42 @@ def diagnose_routing_path_network( "count": len(possible_breaks), } ) + if long_terminal_accesses: + issues.append( + { + "severity": "warning", + "code": "long_terminal_accesses", + "message": "Some terminal access carriers are unusually long.", + "count": len(long_terminal_accesses), + } + ) + if invalid_terminal_local_routes: + issues.append( + { + "severity": "warning", + "code": "invalid_terminal_local_routes", + "message": "Some terminals have invalid local route point metadata.", + "count": len(invalid_terminal_local_routes), + } + ) + if routing_range_only_network: + issues.append( + { + "severity": "warning", + "code": "routing_range_only_network", + "message": "Routing path network only contains fallback routing ranges.", + "count": int(routing_range_only_network.get("routing_range_carriers", 0) or 0), + } + ) + if invalid_route_carriers: + issues.append( + { + "severity": "error", + "code": "invalid_route_carriers", + "message": "Some route carriers have invalid or degenerate geometry.", + "count": len(invalid_route_carriers), + } + ) return { "summary": summary, @@ -3228,6 +3419,10 @@ def diagnose_routing_path_network( "components": components, "isolated_components": isolated_components, "unconnected_terminals": unconnected_terminals, + "long_terminal_accesses": long_terminal_accesses, + "invalid_terminal_local_routes": invalid_terminal_local_routes, + "routing_range_only_network": routing_range_only_network, + "invalid_route_carriers": invalid_route_carriers, "possible_breaks": possible_breaks, "issues": issues, "ok": not issues, @@ -3244,11 +3439,28 @@ def _highlight_routing_network_diagnostics(doc, diagnostic): for item in diagnostic.get("unconnected_terminals", []) or [] if item.get("name", "") ) + long_access_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("long_terminal_accesses", []) or [] + if item.get("name", "") + ) + unconnected_terminal_names.update(long_access_terminal_names) + invalid_local_route_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("invalid_terminal_local_routes", []) or [] + if item.get("name", "") + ) + unconnected_terminal_names.update(invalid_local_route_terminal_names) break_carriers = set( item.get("carrier", {}).get("name", "") for item in diagnostic.get("possible_breaks", []) or [] if item.get("carrier", {}).get("name", "") ) + break_carriers.update( + item.get("carrier", {}).get("name", "") + for item in diagnostic.get("invalid_route_carriers", []) or [] + if item.get("carrier", {}).get("name", "") + ) for obj in list(getattr(doc, "Objects", []) or []): name = getattr(obj, "Name", "") diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 5ab3b89..58e26b0 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1252,6 +1252,57 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, len(carriers)) self.assertEqual("UserPath", carriers[0].QetRouteCarrierKind) + 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") + 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), + ] + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) + + 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], + ) + + 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") + 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] + + self.assertEqual(5, carrier.QetRouteCarrierCapacity) + 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() @@ -1693,6 +1744,120 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("端子接入最大距离 1000.0 mm", message) self.assertIn("补一段线槽/辅助路径", message) + 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() + 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)) + routing_network.create_route_carrier( + doc, + [app.Vector(900, 0, 20), app.Vector(1000, 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, + ) + + result = auto_routing.check_eplan_routing_path_network( + 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"])) + self.assertEqual("terminal-long-access", payload["long_terminal_accesses"][0]["terminal_uuid"]) + self.assertEqual(900.0, payload["long_terminal_accesses"][0]["terminal_access_length_mm"]) + self.assertIn("端子接入过长", message) + self.assertIn("900.0 mm", message) + + 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() + 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" + 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, + ) + + 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, 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.assertIn("端子局部路径无效", message) + self.assertIn("terminal-invalid-local-path", message) + + def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + 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]]) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + ) + 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() @@ -1730,6 +1895,76 @@ class AutoRoutingTest(unittest.TestCase): 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, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="坏用户路径", + project_uuid="project-1", + kind="UserPath", + ) + carrier.Points = [app.Vector(0, 0, 20)] + + 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, 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() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + 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() @@ -2692,6 +2927,47 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("wire-a", diagnostic_payload["route_samples"][0]["wire_uuid"]) self.assertEqual("Routed", diagnostic_payload["route_samples"][0]["route_status"]) + 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", + } + ], + } + + 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(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"], + ) + def test_route_eplan_connections_reports_total_connection_route_length(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -3108,6 +3384,49 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, route["network"]["carriers"]) self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + def test_route_eplan_connections_report_includes_routing_path_network_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, "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-range-only", + "wire_label": "N-RANGE", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + options={"hide_route_carriers_after_route": False}, + project_uuid="project-1", + ) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual(1, report["routed"]) + self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) + self.assertIn( + "routing_range_only_network", + report["routing_path_network_diagnostic"]["issue_codes"], + ) + self.assertIn("路径网络检查提示", message) + self.assertIn("仅使用布线面兜底", message) + def test_route_eplan_connections_preserves_endpoint_metadata_on_routed_wire(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() From daf16df9d6ea3999e3113821e8197e7c66022f50 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Wed, 3 Jun 2026 11:40:24 +0800 Subject: [PATCH 57/63] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81QET=E7=AB=AF?= =?UTF-8?q?=E5=AD=90=E6=8E=92=E5=92=8C=E6=96=AD=E8=B7=AF=E5=99=A8=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E6=8E=92=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 机柜装配操作文档.md | 44 +- ...06-02-batch-din-device-placement-design.md | 181 +++++-- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 2 +- src/Mod/FreeCADExchange/BatchAssembly.py | 487 +++++++++++++++++- src/Mod/FreeCADExchange/ManualWiringPanel.py | 158 ++++-- .../freecad_exchange_batch_assembly_test.py | 160 ++++++ ...eecad_exchange_manual_wiring_panel_test.py | 71 +++ 7 files changed, 1010 insertions(+), 93 deletions(-) diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index 1a6a638..00c30d9 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -146,6 +146,8 @@ 导轨也通常不需要端子 LCS。它是安装基准件。 +当前版本会优先按 QET 传入语义、对象名称、Label、模型路径自动识别导轨。名称中包含 `rail`、`din`、`导轨` 等关键词时,系统会自动补充导轨语义;`标记为导轨` 按钮主要用于纠错或兜底。 + 本仓库已有示例资产: ```text @@ -166,7 +168,9 @@ data/examples/qet_cabinet_assets/qet_wire_duct.FCStd data/examples/qet_cabinet_assets/qet_wire_duct.step ``` -线槽需要在工程里标记为“线槽”,这样布线连接或路径分析才能把它当作走线路径参考。 +线槽需要在工程里具备“线槽”语义,这样布线连接或路径分析才能把它当作走线路径参考。 + +当前版本会优先按 QET 传入语义、对象名称、Label、模型路径自动识别线槽。名称中包含 `wire_duct`、`duct`、`trunking`、`线槽` 等关键词时,系统会自动补充线槽语义;`标记为线槽` 按钮主要用于纠错或兜底。 ### 3.4 有接线点的设备 @@ -459,10 +463,19 @@ QET模板 -> 导入模板实例 ### 9.2 摆放断路器 -1. 导入小型断路器 FCStd 模板。 -2. 移动到导轨前方。 -3. 用 `Assembly` 对齐到导轨。 -4. 多个断路器并排时,使用固定间距复制。 +正式 QET 工程中,如果 QET 已经传入真实断路器设备,不要再重复批量生成断路器。推荐操作: + +1. 从 QET 点击 `3D视图` 打开 FreeCAD,确认树目录中已经有断路器设备。 +2. 选中要安装断路器的导轨。 +3. 切换到 `QET模板`。 +4. 打开 `3D手动布线`。 +5. 点击 `批量断路器`。 +6. 在 `QET断路器前缀` 中输入实际设备前缀,例如 `QF`。 +7. 输入断路器间距和起始偏移。 +8. 确认后,系统会把 QET 已导入的真实断路器沿导轨排布。 +9. 如果状态提示 `已排布 QET 断路器`,说明没有生成假设备,原有 QET 绑定仍保留。 + +只有当前工程没有 QET 断路器数据、只是做 3D 演示时,才使用兜底数量和兜底端子号生成本地演示对象。 常见间距: @@ -474,7 +487,20 @@ QET模板 -> 导入模板实例 ### 9.3 摆放接线端子 -端子片可按固定间距复制。 +正式 QET 工程中,端子排通常已经由 QET 传入到 FreeCAD,例如树目录中出现 `UD:1`、`UD:2`、`ID:6` 这类端子片设备。此时不要再手动复制一批新端子片,应该排布 QET 已导入的真实端子片。 + +推荐操作: + +1. 从 QET 点击 `3D视图` 打开 FreeCAD。 +2. 确认树目录中已经有 `UD`、`ID` 等端子排相关设备。 +3. 选中要安装端子排的导轨。 +4. 切换到 `QET模板`。 +5. 打开 `3D手动布线`。 +6. 点击 `批量端子排`。 +7. 在 `QET端子排名称/前缀` 中输入 `UD` 或 `ID`。 +8. 输入端子片间距,例如 `5.2 mm`,以及起始偏移。 +9. 确认后,系统会把匹配的 QET 真实端子片沿导轨按顺序排布。 +10. 如果状态提示 `已排布 QET 端子排`,说明工程端子和 `terminal_uuid` 没有被替换成本地端子。 例如本仓库生成的端子片: @@ -488,13 +514,13 @@ data/examples/qet_terminal_block/qet_terminal_slice.FCStd 5.2 mm ``` -操作建议: +无 QET 数据的手工演示流程: 1. 导入一个端子片。 2. 移动到导轨上。 3. 用 Draft 阵列或 Link 复制。 4. X 方向间距设为 `5.2 mm`。 -5. 需要端子排编号时,后续在 QET 2D 侧维护端子 UUID 和端子名称。 +5. 这种方式生成的端子通常是本地演示端子,不作为正式 QET 布线匹配主流程。 ### 9.4 摆放电流互感器 @@ -812,7 +838,7 @@ QET 侧只依赖最小绑定字段找到对应设备和端子。 2. 常用设备都整理成 FCStd 模板。 3. 有接线点的设备一定补模板端子。 4. 导轨、线槽、机柜可作为纯几何资产。 -5. 端子排优先用单片端子复制,不要每次重建。 +5. 正式 QET 工程中,端子排和断路器优先排布 QET 已导入的真实实例;Draft 阵列只作为无 QET 数据时的手工演示方式。 6. 每完成一段装配就保存一次 `scene.FCStd`。 7. 布线前先生成工程端子。 8. 生成布线连接前先建立布线路径网络。 diff --git a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md index 201bdd2..9e54fef 100644 --- a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md +++ b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md @@ -2,88 +2,173 @@ ## 目标 -第一版只做当前项目可演示、可操作的批量装配能力:用户在 FreeCAD 中选中一根已标记的导轨后,可以批量插入端子片形成端子排,也可以批量插入小型断路器。系统按导轨方向等距放置对象,并为每个对象生成可布线工程端子。 +本功能用于 QET 与 FreeCAD 协同工程中的快速 3D 装配。正式工程里,QET 已经传入真实设备、真实端子和 3D 模型,FreeCAD 不再把 `批量端子排`、`批量断路器` 理解为重新生成一批假设备,而是把 QET 已导入的真实实例沿导轨批量排布。 -本功能不做完整设备库、不扩展数据库绑定表、不替代 QET 现有 2D 设备/符号关联。2D 仍负责设备型号、端子号和导线任务;3D 只负责模型、位姿、端子空间点和装配状态。 +第一版目标: -## 范围 +- 选择一根 DIN 导轨后,批量排布 QET 已导入的端子排实例,例如 `UD`、`ID`。 +- 选择一根 DIN 导轨后,批量排布 QET 已导入的小型断路器或同类设备,例如 `QF1`、`QF2`。 +- 保留 QET 身份字段,尤其是 `QetTerminalUuid`、`QetInstanceId`、`QetElementUuid`。 +- 不破坏现有工程端子、导线任务和后续布线匹配。 +- 旧的本地占位生成逻辑只作为没有 QET 数据时的演示兜底。 -### 端子排 +## 数据职责 -用户选择导轨后,输入端子排名称、端子数量、端子片宽度/间距和起始偏移。系统沿导轨方向生成: +QET 负责: -- 一个端子排分组,例如 `XT1` -- 多个端子片实例,例如 `XT1_001`、`XT1_002`。这些实例同时写入标准 QET 设备语义,便于后续端子绑定和布线索引识别。 -- 每片端子对应的工程端子,例如 `XT1:1`、`XT1:2` +- 2D 原理图中的设备、符号、端子、端子排和导线任务。 +- 设备型号、端子号、端子排名称,例如 `UD`、`ID`。 +- 设备与 3D 模型资产绑定。 +- 端子的真实 `terminal_uuid`。 -正式主流程中,端子片模型资源应来自 QET 传入的设备/资产绑定。后端保留 `model_path` 参数,用于 QET 自动传入模型路径或开发调试兜底;普通用户参数窗口不要求手动选择模型文件。未传入模型路径时回退为脚本生成的简化几何。关键是位置整齐、命名清楚、可被布线模块发现。 +FreeCAD 负责: -### 小型断路器 +- 真实 3D 设备实例的空间位姿。 +- 导轨、线槽、柜面等装配宿主。 +- 工程端子的 3D 坐标和出线方向。 +- 设备与导轨的批量排布状态。 +- 3D 布线路径和保存回写。 -用户选择导轨后,输入起始设备名、数量、单个宽度/间距和端子号模板。系统沿导轨方向生成: +第一版仍遵守 2D/3D 协同约束:3D 端子绑定唯一依据是 `terminal_uuid`,3D 位姿以 `scene.FCStd` 为准,不从数据库反推 3D 位姿。 -- 多个设备实例,例如 `QF1`、`QF2`。对象名称使用 `QETDevice_QF1` 这类标准前缀,树目录 Label 仍显示 `QF1`。 -- 每个设备的工程端子,例如 `QF1:1`、`QF1:2`、`QF1:3`、`QF1:4`、`QF1:5`、`QF1:6` +## 端子排批量排布 -如果 2D 已经提供真实设备名和端子号,后续导入/绑定逻辑优先使用 QET 的 `terminal_uuid`;本功能生成的是第一版本地 3D 辅助对象,用于快速摆放和演示。 +正式流程: -## 数据语义 +1. 用户在 FreeCAD 中选中一根已识别或已标记的导轨。 +2. 点击 `3D手动布线` 面板中的 `批量端子排`。 +3. 输入 QET 端子排名称或前缀,例如 `UD`、`ID`。 +4. 输入端子片间距和起始偏移。 +5. 系统扫描当前 `scene.FCStd` 中 QET 已导入的端子片设备。 +6. 匹配端子排名称,例如 `UD:1`、`UD-2`、`ID_006`。 +7. 按 QET 顺序字段或名称中的自然序号排序。 +8. 沿导轨轴向排布这些真实端子片。 +9. 写入轻量装配属性,不改变端子的 QET 绑定。 + +端子排匹配优先读取这些属性: + +- `QetTerminalStripName` +- `QetTerminalBlockName` +- `QetTerminalGroupName` +- `QetStripName` +- `QetParentTerminalBlockName` + +如果没有上述属性,则从对象 `Label` / `Name` 解析 `UD:1`、`ID-2` 这类名称。 + +端子排排序优先读取这些属性: + +- `QetTerminalStripIndex` +- `QetTerminalIndex` +- `QetTerminalSequence` +- `QetTerminalOrder` +- `QetTerminalNo` +- `QetTerminalDisplay` + +如果没有上述属性,则从对象名称中提取最后一个数字做自然排序。 + +## 小型断路器批量排布 + +正式流程: + +1. 用户在 FreeCAD 中选中一根导轨。 +2. 点击 `3D手动布线` 面板中的 `批量断路器`。 +3. 输入 QET 设备前缀,例如 `QF`。 +4. 输入设备间距和起始偏移。 +5. 系统扫描当前 `scene.FCStd` 中 QET 已导入的真实设备实例。 +6. 排除端子排端子片和旧的本地批量生成对象。 +7. 按设备 `Label`、`Name`、`QetInstanceId` 等字段匹配前缀。 +8. 按自然顺序排布,例如 `QF1`、`QF2`、`QF10`。 +9. 保留设备下的工程端子和 QET 绑定关系。 + +断路器端子号来自 QET 传入的真实端子数据。参数窗口中的“兜底端子号”只在当前工程没有匹配 QET 设备、需要演示生成占位对象时使用。 + +## 旧兜底逻辑 + +为了保留开发调试和无 QET 数据演示能力,旧接口仍保留: + +- `create_terminal_block(...)` +- `create_breakers(...)` + +但正式按钮调用顺序是: + +```text +先 layout_existing_terminal_block / layout_existing_devices +如果 updated_devices > 0,说明已排布 QET 真实对象 +如果没有匹配对象,才回退 create_terminal_block / create_breakers +``` + +兜底生成对象可能产生 `local:*` 端子,只能用于 3D 演示和开发测试,不作为正式 QET 布线匹配的主流程。 + +## 装配属性 + +排布真实 QET 对象时,系统只写入轻量属性: -- 不新增数据库字段。 -- 3D 对象保存在 FreeCAD 文档中。 -- 工程端子仍使用 `TerminalObjects.set_terminal_semantics(...)`。 -- 批量生成的本地槽位端子使用 `local::`,避免伪造 QET terminal_uuid。 -- 批量设备组写入 `QetProjectUuid`、`QetElementUuid`、`QetInstanceId` 和 `QetGroupKind=Device`,使它们能被现有端子导入/布线绑定逻辑找到。 -- 当 QET 的 `2d_to_3d.json` 后续提供真实 `terminal_uuid + terminal_display` 时,布线/端子绑定逻辑按端子号匹配本地槽位,并把 `local:*` 提升为真实 QET `terminal_uuid`。 -- 树目录显示名使用 `设备名:端子号`,方便设计人员辨认。 -- 批量对象额外写入本地属性: - `QetBatchAssemblyKind` - `QetBatchAssemblyName` +- `QetBatchAssemblyMode = layout_existing` +- `QetBatchAssemblyOrder` +- `QetBatchAssemblyOffsetMm` +- `QetMountKind = rail` - `QetMountHostName` -- `QetMountKind` -- `QetBatchSourceModelPath`,仅导入本地模型文件时写入可见几何对象 +- `QetMountHostKind` + +这些属性保存在 FreeCAD 文档里,用于后续刷新、诊断和显示,不扩展第一版数据库绑定表。 ## 导轨定位规则 -第一版使用导轨对象的 `QetCarrierAxis` 作为排列轴,默认 `x`。放置公式: +第一版使用导轨对象的 `QetCarrierAxis` 作为排列轴,默认 `x`。如果导轨带旋转,排列轴经过导轨 `Placement.Rotation` 转换。 + +放置公式: ```text -第 N 个对象位置 = 导轨 Placement.Base + 轴向单位向量 * (起始偏移 + N * 间距) +第 N 个对象位置 = 导轨 Placement.Base + 导轨轴向单位向量 * (起始偏移 + N * 间距) ``` -如果导轨对象带有旋转,排列轴会经过导轨 `Placement.Rotation` 转换。第一版先保证当前工程和内置导轨的稳定演示。 +当前实现重点保证批量排布稳定、身份不丢失。复杂 Assembly Joint、端子片端挡、隔板、跨接片、短接片规则暂不纳入第一版。 ## UI -挂在 `QET模板 -> 3D手动布线` 面板,新增两个按钮: +入口位于: + +```text +QET模板 -> 3D手动布线 +``` + +按钮: - `批量端子排` - `批量断路器` -点击按钮后弹出参数窗口,窗口内带默认参数: +参数窗口说明: + +- `QET端子排名称/前缀`:正式工程用于匹配 QET 端子排,例如 `UD`、`ID`。 +- `QET断路器前缀`:正式工程用于匹配 QET 已导入设备,例如 `QF`。 +- `端子间距 / 断路器间距`:沿导轨方向的排布间距。 +- `起始偏移`:从导轨基点开始的偏移。 +- `兜底数量 / 兜底端子号`:只有找不到匹配 QET 对象时才用于生成演示对象。 -- 端子排:`XT1`,10 片,5.2 mm 间距 -- 小型断路器:`QF`,3 个,18 mm 间距,端子号 `1,2,3,4,5,6` +执行成功后状态栏会区分: -端子号支持用空格、英文逗号、中文逗号、分号分隔;重复端子号会被拒绝,避免生成两个同名接线点。 -普通用户窗口不提供模型文件选择;模型文件由 QET 侧传入或由开发调试入口传入。不传入时使用脚本简化几何。 +- `已排布 QET 端子排` +- `已排布 QET 断路器` +- `未找到匹配的 QET ...,已兜底生成` ## 验收 -1. 选中导轨后点击 `批量端子排`,生成 `XT1` 分组和端子片。 -2. 在参数窗口中可调整端子排名称、数量、间距和起始偏移。 -3. 每个端子片都有一个工程端子,Label 为 `XT1:n`。 -4. 选中导轨后点击 `批量断路器`,生成 `QF1`、`QF2`、`QF3`。 -5. 在参数窗口中可调整断路器前缀、数量、间距、起始偏移和端子号模板。 -6. 每个断路器生成指定端子号。 -7. 生成对象位于导轨方向上,间距正确。 -8. 端子默认隐藏,但可被手动/自动布线模块按端子对象收集。 -9. 如果 QET 已传入同一设备实例和端子号,`local:*` 槽位能被提升为真实 `terminal_uuid`,从而参与导线任务匹配。 +1. 从 QET 点击 `3D视图` 打开 FreeCAD。 +2. 树目录中已经存在 QET 导入的端子片或设备实例。 +3. 选中导轨,点击 `批量端子排`。 +4. 输入 `UD` 或 `ID`,确认后真实端子片沿导轨排布。 +5. 排布后端子对象仍保留真实 `QetTerminalUuid`,不会变成 `local:*`。 +6. 选中导轨,点击 `批量断路器`。 +7. 输入 `QF`,确认后真实断路器沿导轨排布。 +8. 保存后重新打开 `scene.FCStd`,设备位置保持。 +9. 后续 `3D手动布线` 或 `3D布线连接` 能继续通过 `terminal_uuid` 匹配导线任务。 ## 非目标 -- 不做完整 SolidWorks Electrical 设备库。 -- 不自动读取所有 2D 设备属性批量生成真实设备。 -- 不伪造 QET terminal_uuid;只有 QET 输入中存在真实端子 UUID 时才提升绑定。 -- 不做复杂 Assembly Joint。 -- 不做完整端子排电气跨接片、跳线、短接片规则。 +- 不做完整 SolidWorks Electrical / EPLAN 设备库。 +- 不在 FreeCAD 中重新创建 QET 已经传入的正式设备。 +- 不伪造 QET `terminal_uuid`。 +- 不删除旧兜底生成函数,但普通工程主流程不依赖它。 +- 不实现完整端子排电气跨接片、跳线、端挡和标记条规则。 diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index f3c766c..abbc0dd 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -317,7 +317,7 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.terminal_access_max_distance_spin) - options_layout.addWidget(QtWidgets.QLabel("端子出线长度 mm")) + options_layout.addWidget(QtWidgets.QLabel("自动端子出线长度 mm")) self.terminal_exit_length_spin = QtWidgets.QDoubleSpinBox() self.terminal_exit_length_spin.setRange(0.0, 1000.0) self.terminal_exit_length_spin.setDecimals(1) diff --git a/src/Mod/FreeCADExchange/BatchAssembly.py b/src/Mod/FreeCADExchange/BatchAssembly.py index ecd97d4..2be0012 100644 --- a/src/Mod/FreeCADExchange/BatchAssembly.py +++ b/src/Mod/FreeCADExchange/BatchAssembly.py @@ -1,4 +1,5 @@ import math +import re from pathlib import Path import FreeCAD as App @@ -15,6 +16,33 @@ class BatchAssemblyError(RuntimeError): pass +TERMINAL_STRIP_NAME_PROPERTIES = ( + "QetTerminalStripName", + "QetTerminalBlockName", + "QetTerminalGroupName", + "QetStripName", + "QetParentTerminalBlockName", +) + +TERMINAL_STRIP_ORDER_PROPERTIES = ( + "QetTerminalStripIndex", + "QetTerminalIndex", + "QetTerminalSequence", + "QetTerminalOrder", + "QetTerminalNo", + "QetTerminalDisplay", +) + +DEVICE_PREFIX_PROPERTIES = ( + "QetDeviceTag", + "QetDeviceName", + "QetDisplayTag", + "QetSymbolLabel", + "QetInstanceId", + "QetElementUuid", +) + + def _project_uuid(doc): try: root = TerminalObjects.ensure_root_group(doc) @@ -28,6 +56,198 @@ def _safe_label(text, fallback): return value or fallback +def _text_values(obj, include_children=False): + values = [] + for attr_name in ("Label", "Name"): + value = (getattr(obj, attr_name, "") or "").strip() + if value: + values.append(value) + for prop_name in DEVICE_PREFIX_PROPERTIES + TERMINAL_STRIP_NAME_PROPERTIES: + value = (getattr(obj, prop_name, "") or "").strip() + if value: + values.append(value) + if include_children: + for child in list(getattr(obj, "Group", []) or []): + for attr_name in ("Label", "Name"): + value = (getattr(child, attr_name, "") or "").strip() + if value: + values.append(value) + return values + + +def _natural_sort_key(value): + text = str(value or "") + key = [] + for part in re.split(r"(\d+)", text): + if part.isdigit(): + key.append((0, int(part))) + else: + key.append((1, part.lower())) + return key + + +def _parse_strip_name_and_order(obj): + for prop_name in TERMINAL_STRIP_NAME_PROPERTIES: + strip_name = (getattr(obj, prop_name, "") or "").strip() + if not strip_name: + continue + order = _explicit_order(obj) + if order is None: + order = _order_from_texts(_text_values(obj)) + return strip_name, order + + for text in _text_values(obj): + # Examples from QET trees: UD:1, UD-2, ID_006. + match = re.match(r"^\s*([A-Za-z][A-Za-z0-9]{0,8})\s*[::_\-]\s*(\d+)\b", text) + if match: + return match.group(1), int(match.group(2)) + return "", None + + +def _explicit_order(obj): + for prop_name in TERMINAL_STRIP_ORDER_PROPERTIES: + value = (getattr(obj, prop_name, "") or "").strip() + if not value: + continue + match = re.search(r"\d+", value) + if match: + return int(match.group(0)) + return None + + +def _order_from_texts(texts): + for text in texts: + match = re.search(r"(\d+)(?!.*\d)", str(text or "")) + if match: + return int(match.group(1)) + return None + + +def _is_group_like(obj): + try: + return bool(obj and obj.isDerivedFrom("App::DocumentObjectGroup")) + except Exception: + return bool(getattr(obj, "Group", None) is not None) + + +def _qet_identity(obj): + instance_id = (getattr(obj, "QetInstanceId", "") or "").strip() + element_uuid = (getattr(obj, "QetElementUuid", "") or "").strip() + return instance_id, element_uuid + + +def _is_qet_device_object(obj): + if obj is None: + return False + name = getattr(obj, "Name", "") or "" + instance_id, element_uuid = _qet_identity(obj) + if name.startswith(TerminalObjects.DEVICE_GROUP_PREFIX): + return True + return bool(instance_id or element_uuid) + + +def _contains_terminal_slice_geometry(obj): + text = " ".join(_text_values(obj, include_children=True)).lower() + return any( + token in text + for token in ( + "terminalslice", + "terminal_slice", + "terminal slice", + "get_terminal_slice", + "端子片", + "端子排", + ) + ) + + +def _contains_qet_terminal_group(obj): + for child in list(getattr(obj, "Group", []) or []): + if (getattr(child, "QetGroupKind", "") or "").strip() == TerminalObjects.TERMINAL_GROUP_KIND: + return True + if getattr(child, "Name", "").startswith(TerminalObjects.TERMINAL_GROUP_PREFIX): + return True + return False + + +def _is_terminal_strip_device(obj): + if not _is_qet_device_object(obj): + return False + strip_name, _order = _parse_strip_name_and_order(obj) + if not strip_name: + return False + if _contains_terminal_slice_geometry(obj): + return True + if _contains_qet_terminal_group(obj): + return True + return False + + +def _matches_prefix(obj, prefix): + prefix = (prefix or "").strip().lower() + if not prefix: + return True + for text in _text_values(obj): + if text.lower().startswith(prefix): + return True + return False + + +def _is_batch_generated(obj): + return bool((getattr(obj, "QetBatchAssemblyKind", "") or "").strip()) + + +def _existing_terminal_strip_devices(doc, strip_name=""): + wanted = (strip_name or "").strip().lower() + devices = [] + for obj in list(getattr(doc, "Objects", []) or []): + if not _is_terminal_strip_device(obj): + continue + current_strip, order = _parse_strip_name_and_order(obj) + if wanted and current_strip.lower() != wanted: + continue + devices.append((current_strip, order, obj)) + devices.sort( + key=lambda item: ( + item[0].lower(), + item[1] if item[1] is not None else 10**9, + _natural_sort_key(getattr(item[2], "Label", "") or getattr(item[2], "Name", "")), + ) + ) + return [obj for _strip, _order, obj in devices] + + +def available_terminal_strip_names(doc): + names = [] + seen = set() + for obj in list(getattr(doc, "Objects", []) or []): + if not _is_terminal_strip_device(obj): + continue + strip_name, _order = _parse_strip_name_and_order(obj) + key = strip_name.lower() + if strip_name and key not in seen: + seen.add(key) + names.append(strip_name) + names.sort(key=_natural_sort_key) + return names + + +def _existing_devices_by_prefix(doc, prefix=""): + devices = [] + for obj in list(getattr(doc, "Objects", []) or []): + if not _is_qet_device_object(obj): + continue + if _is_terminal_strip_device(obj): + continue + if _is_batch_generated(obj): + continue + if not _matches_prefix(obj, prefix): + continue + devices.append(obj) + devices.sort(key=lambda obj: _natural_sort_key(getattr(obj, "Label", "") or getattr(obj, "Name", ""))) + return devices + + def _axis_vector(rail): axis = (getattr(rail, "QetCarrierAxis", "") or "x").strip().lower() if axis == "y": @@ -76,6 +296,103 @@ def _placement_at(rail, point): return App.Placement(point, rotation or App.Rotation()) +def _vector_copy(vector): + return App.Vector( + float(getattr(vector, "x", 0.0) or 0.0), + float(getattr(vector, "y", 0.0) or 0.0), + float(getattr(vector, "z", 0.0) or 0.0), + ) + + +def _vector_add(left, right): + return App.Vector( + float(getattr(left, "x", 0.0) or 0.0) + float(getattr(right, "x", 0.0) or 0.0), + float(getattr(left, "y", 0.0) or 0.0) + float(getattr(right, "y", 0.0) or 0.0), + float(getattr(left, "z", 0.0) or 0.0) + float(getattr(right, "z", 0.0) or 0.0), + ) + + +def _vector_sub(left, right): + return App.Vector( + float(getattr(left, "x", 0.0) or 0.0) - float(getattr(right, "x", 0.0) or 0.0), + float(getattr(left, "y", 0.0) or 0.0) - float(getattr(right, "y", 0.0) or 0.0), + float(getattr(left, "z", 0.0) or 0.0) - float(getattr(right, "z", 0.0) or 0.0), + ) + + +def _placement_base(obj): + placement = getattr(obj, "Placement", None) + base = getattr(placement, "Base", None) + if base is None: + return None + return _vector_copy(base) + + +def _placement_controls_children(obj): + try: + return bool(obj is not None and obj.isDerivedFrom("App::Part")) + except Exception: + return False + + +def _iter_transform_children(obj): + for child in list(getattr(obj, "Group", []) or []): + yield child + if not _placement_controls_children(child): + for nested in _iter_transform_children(child): + yield nested + + +def _object_anchor_point(obj): + if obj is None: + return App.Vector(0, 0, 0) + children = list(getattr(obj, "Group", []) or []) + if _placement_controls_children(obj) or not children: + return _placement_base(obj) or App.Vector(0, 0, 0) + + points = [] + for child in _iter_transform_children(obj): + base = _placement_base(child) + if base is not None: + points.append(base) + if not points: + return _placement_base(obj) or App.Vector(0, 0, 0) + + return App.Vector( + sum(point.x for point in points) / len(points), + sum(point.y for point in points) / len(points), + sum(point.z for point in points) / len(points), + ) + + +def _translate_placement(obj, delta): + placement = getattr(obj, "Placement", None) + base = getattr(placement, "Base", None) + if placement is None or base is None: + return False + new_base = _vector_add(base, delta) + try: + placement.Base = new_base + obj.Placement = placement + return True + except Exception: + try: + obj.Placement = App.Placement(new_base, getattr(placement, "Rotation", App.Rotation())) + return True + except Exception: + return False + + +def _translate_group_children(obj, delta): + moved = 0 + for child in list(getattr(obj, "Group", []) or []): + if _translate_placement(child, delta): + moved += 1 + if not _placement_controls_children(child): + moved += _translate_group_children(child, delta) + return moved + + def _ensure_rail(rail): if rail is None: raise BatchAssemblyError("请先选择一根导轨。") @@ -222,6 +539,67 @@ def _set_batch_properties(obj, kind, batch_name, host): "Mount host object name", getattr(host, "Name", "") or "", ) + TerminalObjects.ensure_string_property( + obj, + "QetMountHostKind", + "QET Mount", + "Mount host kind", + (getattr(host, "QetCarrierKind", "") or "").strip(), + ) + + +def _set_layout_properties(obj, kind, batch_name, host, order_index, offset_mm): + _set_batch_properties(obj, kind, batch_name, host) + TerminalObjects.ensure_string_property( + obj, + "QetBatchAssemblyMode", + "QET Batch Assembly", + "Batch assembly mode", + "layout_existing", + ) + TerminalObjects.ensure_string_property( + obj, + "QetBatchAssemblyOrder", + "QET Batch Assembly", + "Batch assembly order", + str(int(order_index)), + ) + TerminalObjects.ensure_string_property( + obj, + "QetBatchAssemblyOffsetMm", + "QET Batch Assembly", + "Batch assembly offset in millimeters", + "{0:.6f}".format(float(offset_mm or 0.0)), + ) + + +def _set_object_placement(obj, placement): + if obj is not None and getattr(obj, "Group", None) and not _placement_controls_children(obj): + current = _object_anchor_point(obj) + target = getattr(placement, "Base", App.Vector(0, 0, 0)) + delta = _vector_sub(target, current) + moved_children = _translate_group_children(obj, delta) + try: + obj.Placement = placement + except Exception: + pass + if moved_children: + return True + + try: + obj.Placement = placement + return True + except Exception: + try: + existing = getattr(obj, "Placement", None) + if existing is not None: + existing.Base = placement.Base + existing.Rotation = placement.Rotation + obj.Placement = existing + return True + except Exception: + pass + return False def _set_qet_device_properties(obj, project_uuid, element_uuid, instance_id): @@ -367,7 +745,17 @@ def _create_terminal( return terminal -def _batch_report(kind, group, devices, terminals): +def _terminal_objects_for_devices(devices): + terminals = [] + for device in devices or []: + try: + terminals.extend(TerminalObjects.collect_terminal_objects(device)) + except Exception: + pass + return terminals + + +def _batch_report(kind, group, devices, terminals, source="fallback_created"): return { "kind": kind, "group": group, @@ -375,9 +763,106 @@ def _batch_report(kind, group, devices, terminals): "terminals": terminals, "created_devices": len(devices), "created_terminals": len(terminals), + "updated_devices": 0, + "updated_terminals": 0, + "source": source, } +def _layout_existing_objects( + doc, + rail, + objects, + kind, + batch_name, + pitch_mm, + start_offset_mm, +): + rail = _ensure_rail(rail) + objects = [obj for obj in objects or [] if obj is not None] + if not objects: + return _batch_report(kind, rail, [], [], source="qet_existing") + + base = _base_point(rail) + axis = _axis_vector(rail) + updated = [] + for index, obj in enumerate(objects): + offset = float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0) + placement = _placement_at(rail, _point_at(base, axis, offset)) + if _set_object_placement(obj, placement): + _set_layout_properties(obj, kind, batch_name, rail, index + 1, offset) + updated.append(obj) + + try: + doc.recompute() + except Exception: + pass + + terminals = _terminal_objects_for_devices(updated) + return { + "kind": kind, + "group": rail, + "devices": updated, + "terminals": terminals, + "created_devices": 0, + "created_terminals": 0, + "updated_devices": len(updated), + "updated_terminals": len(terminals), + "source": "qet_existing", + } + + +def layout_existing_terminal_block( + doc, + rail, + block_name="", + pitch_mm=5.2, + start_offset_mm=0.0, +): + if doc is None: + raise BatchAssemblyError("请先打开 FreeCAD 工程。") + rail = _ensure_rail(rail) + devices = _existing_terminal_strip_devices(doc, block_name) + if not devices: + return _batch_report("terminal_block", rail, [], [], source="qet_existing") + batch_name = _safe_label(block_name, _parse_strip_name_and_order(devices[0])[0] or "QET端子排") + return _layout_existing_objects( + doc, + rail, + devices, + "terminal_block", + batch_name, + pitch_mm, + start_offset_mm, + ) + + +def layout_existing_devices( + doc, + rail, + prefix="QF", + pitch_mm=18.0, + start_offset_mm=0.0, + kind="device_batch", +): + if doc is None: + raise BatchAssemblyError("请先打开 FreeCAD 工程。") + rail = _ensure_rail(rail) + devices = _existing_devices_by_prefix(doc, prefix) + if not devices: + return _batch_report(kind, rail, [], [], source="qet_existing") + batch_name = _safe_label(prefix, "QET设备") + return _layout_existing_objects( + doc, + rail, + devices, + kind, + batch_name, + pitch_mm, + start_offset_mm, + ) + + def create_terminal_block( doc, rail, diff --git a/src/Mod/FreeCADExchange/ManualWiringPanel.py b/src/Mod/FreeCADExchange/ManualWiringPanel.py index 19c4716..fc5598e 100644 --- a/src/Mod/FreeCADExchange/ManualWiringPanel.py +++ b/src/Mod/FreeCADExchange/ManualWiringPanel.py @@ -140,13 +140,31 @@ def _dialog_accepted_value(): return 1 +def _combo_or_line_text(widget): + if hasattr(widget, "currentText"): + return widget.currentText() + if hasattr(widget, "text"): + return widget.text() + return "" + + def _prompt_terminal_block_options(parent=None): if QtWidgets is None: raise ManualWiringPanelError("当前 FreeCAD 未加载 Qt,不能打开批量端子排参数窗口。") dialog = QtWidgets.QDialog(parent) dialog.setWindowTitle("批量端子排") layout = QtWidgets.QFormLayout(dialog) - name_input = QtWidgets.QLineEdit(DEFAULT_BATCH_TERMINAL_BLOCK_NAME) + strip_names = [] + try: + strip_names = BatchAssembly.available_terminal_strip_names(_active_document()) + except Exception: + strip_names = [] + if strip_names and hasattr(QtWidgets, "QComboBox"): + name_input = QtWidgets.QComboBox() + name_input.setEditable(True) + name_input.addItems(strip_names) + else: + name_input = QtWidgets.QLineEdit(DEFAULT_BATCH_TERMINAL_BLOCK_NAME) count_input = QtWidgets.QSpinBox() count_input.setRange(1, 10000) count_input.setValue(DEFAULT_BATCH_TERMINAL_BLOCK_COUNT) @@ -158,8 +176,8 @@ def _prompt_terminal_block_options(parent=None): offset_input.setRange(-100000.0, 100000.0) offset_input.setDecimals(2) offset_input.setValue(0.0) - layout.addRow("端子排名称", name_input) - layout.addRow("端子数量", count_input) + layout.addRow("QET端子排名称/前缀", name_input) + layout.addRow("兜底端子数量", count_input) layout.addRow("端子间距 mm", pitch_input) layout.addRow("起始偏移 mm", offset_input) buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) @@ -169,7 +187,7 @@ def _prompt_terminal_block_options(parent=None): if _exec_dialog(dialog) != _dialog_accepted_value(): return None return _batch_terminal_block_options( - name_input.text(), + _combo_or_line_text(name_input), count_input.value(), pitch_input.value(), offset_input.value(), @@ -195,11 +213,11 @@ def _prompt_breaker_options(parent=None): offset_input.setDecimals(2) offset_input.setValue(0.0) terminals_input = QtWidgets.QLineEdit(DEFAULT_BATCH_BREAKER_TERMINALS_TEXT) - layout.addRow("断路器前缀", name_input) - layout.addRow("断路器数量", count_input) + layout.addRow("QET断路器前缀", name_input) + layout.addRow("兜底断路器数量", count_input) layout.addRow("断路器间距 mm", pitch_input) layout.addRow("起始偏移 mm", offset_input) - layout.addRow("端子号", terminals_input) + layout.addRow("兜底端子号", terminals_input) buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) @@ -898,6 +916,8 @@ def _carrier_kind_from_object(obj): if obj is not None: text_parts.append(getattr(obj, "Name", "") or "") text_parts.append(getattr(obj, "Label", "") or "") + text_parts.append(getattr(obj, "QetCarrierSourcePath", "") or "") + text_parts.append(getattr(obj, "QetResolvedModelPath", "") or "") text = " ".join(text_parts).lower() if "线槽" in text or "duct" in text or "trunking" in text: return "wire_duct" @@ -908,17 +928,36 @@ def _carrier_kind_from_object(obj): return "" +def _auto_mark_carrier_if_detected(doc, obj): + if obj is None: + return None + carrier = _carrier_object_from_object(obj) + if carrier is not None: + return carrier + + carrier_kind = _carrier_kind_from_object(obj) + if carrier_kind not in CARRIER_ROLE_LABELS: + return None + + _set_carrier_properties(obj, carrier_kind) + try: + carrier_group = WiringObjects.ensure_carrier_group(doc or _active_document()) + if obj not in getattr(carrier_group, "Group", []): + carrier_group.addObject(obj) + except Exception: + pass + return obj + + def _carrier_object_from_object(obj): - candidates = [] current = obj - if current is not None: - candidates.append(current) - candidates.extend(list(getattr(current, "InList", []) or [])) - - for candidate in candidates: - carrier_kind = (getattr(candidate, "QetCarrierKind", "") or "").strip() - if carrier_kind: - return candidate + visited = set() + while current is not None and id(current) not in visited: + visited.add(id(current)) + if (getattr(current, "QetCarrierKind", "") or "").strip(): + return current + parents = list(getattr(current, "InList", []) or []) + current = parents[0] if parents else None return None @@ -943,11 +982,25 @@ def _carrier_role_label(carrier_kind): def _selected_carrier_objects(): - return [ - obj - for obj in _selection() - if obj is not None and not TerminalObjects.is_terminal_object(obj) - ] + selected = [] + for obj in _selection(): + if obj is not None and not TerminalObjects.is_terminal_object(obj): + selected.append(obj) + for picked in _selection_ex(): + obj = getattr(picked, "Object", None) + if obj is not None and not TerminalObjects.is_terminal_object(obj): + selected.append(obj) + + carriers = [] + try: + doc = _active_document() + except Exception: + doc = None + for obj in selected: + carrier = _auto_mark_carrier_if_detected(doc, obj) or _carrier_object_from_object(obj) or obj + if carrier not in carriers: + carriers.append(carrier) + return carriers def _selected_rail_object(): @@ -1505,8 +1558,18 @@ class ManualWiringController: rail = _selected_rail_object() if rail is None: raise ManualWiringPanelError("请先选择一根已标记的导轨。") + doc = _active_document() + report = BatchAssembly.layout_existing_terminal_block( + doc, + rail, + block_name=block_name, + pitch_mm=pitch_mm, + start_offset_mm=start_offset_mm, + ) + if report.get("updated_devices", 0): + return report return BatchAssembly.create_terminal_block( - _active_document(), + doc, rail, block_name=block_name, count=count, @@ -1527,8 +1590,19 @@ class ManualWiringController: rail = _selected_rail_object() if rail is None: raise ManualWiringPanelError("请先选择一根已标记的导轨。") + doc = _active_document() + report = BatchAssembly.layout_existing_devices( + doc, + rail, + prefix=base_name, + pitch_mm=pitch_mm, + start_offset_mm=start_offset_mm, + kind="breaker_batch", + ) + if report.get("updated_devices", 0): + return report return BatchAssembly.create_breakers( - _active_document(), + doc, rail, base_name=base_name, count=count, @@ -1848,7 +1922,7 @@ class ManualWiringTaskPanel: layout.addWidget(self.use_task_button) layout.addWidget(self.reload_tasks_button) exit_layout = QtWidgets.QHBoxLayout() - exit_layout.addWidget(QtWidgets.QLabel("端子出线长度")) + exit_layout.addWidget(QtWidgets.QLabel("手动端子出线长度")) exit_layout.addWidget(self.exit_length_input) layout.addLayout(exit_layout) carrier_length_layout = QtWidgets.QHBoxLayout() @@ -2066,12 +2140,20 @@ class ManualWiringTaskPanel: if options is None: return report = self.controller.create_terminal_block_from_selection(**options) - self._set_status( - "已批量生成端子排:设备 {0} 个,工程端子 {1} 个。".format( - report.get("created_devices", 0), - report.get("created_terminals", 0), + if report.get("source") == "qet_existing": + self._set_status( + "已排布 QET 端子排:设备 {0} 个,工程端子 {1} 个。".format( + report.get("updated_devices", 0), + report.get("updated_terminals", 0), + ) + ) + else: + self._set_status( + "未找到匹配的 QET 端子排,已兜底生成:设备 {0} 个,工程端子 {1} 个。".format( + report.get("created_devices", 0), + report.get("created_terminals", 0), + ) ) - ) except Exception as exc: self._set_error(str(exc)) @@ -2081,12 +2163,20 @@ class ManualWiringTaskPanel: if options is None: return report = self.controller.create_breakers_from_selection(**options) - self._set_status( - "已批量生成小型断路器:设备 {0} 个,工程端子 {1} 个。".format( - report.get("created_devices", 0), - report.get("created_terminals", 0), + if report.get("source") == "qet_existing": + self._set_status( + "已排布 QET 断路器:设备 {0} 个,工程端子 {1} 个。".format( + report.get("updated_devices", 0), + report.get("updated_terminals", 0), + ) + ) + else: + self._set_status( + "未找到匹配的 QET 断路器,已兜底生成:设备 {0} 个,工程端子 {1} 个。".format( + report.get("created_devices", 0), + report.get("created_terminals", 0), + ) ) - ) except Exception as exc: self._set_error(str(exc)) diff --git a/tests/python/freecad_exchange_batch_assembly_test.py b/tests/python/freecad_exchange_batch_assembly_test.py index c5f8c07..123ab60 100644 --- a/tests/python/freecad_exchange_batch_assembly_test.py +++ b/tests/python/freecad_exchange_batch_assembly_test.py @@ -124,6 +124,166 @@ def _reload_modules(*extra_names): class BatchAssemblyTest(unittest.TestCase): + def _qet_device(self, doc, terminal_objects, label, instance_id=None, element_uuid=None): + token = terminal_objects.safe_token(label) + device = doc.addObject("App::DocumentObjectGroup", "QETDevice_" + token) + device.Label = label + terminal_objects.ensure_string_property(device, "QetGroupKind", "QET Exchange", "", "Device") + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "", + element_uuid or label, + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "", + instance_id or label, + ) + return device + + def _terminal(self, doc, terminal_objects, device, terminal_uuid, label): + terminal_group = terminal_objects.ensure_terminal_group( + doc, + device, + project_uuid="project-1", + instance_id=device.QetInstanceId, + ) + terminal = terminal_objects.create_lcs_object( + doc, + "QETTerminal_" + terminal_objects.safe_token(terminal_uuid), + label=label, + ) + terminal_group.addObject(terminal) + terminal_objects.set_terminal_semantics( + terminal, + "project-1", + device.QetElementUuid, + terminal_uuid, + device.QetInstanceId, + label=label, + ) + return terminal + + def test_layout_existing_terminal_block_places_qet_terminal_slices_without_local_rebind(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + ud2 = self._qet_device(doc, terminal_objects, "UD:2", instance_id="ud-2", element_uuid="element-ud-2") + ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1") + self._terminal(doc, terminal_objects, ud2, "terminal-ud-2", "UD:2") + self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1") + + report = batch_assembly.layout_existing_terminal_block( + doc, + rail, + block_name="UD", + pitch_mm=5.2, + start_offset_mm=10.0, + ) + + self.assertEqual("qet_existing", report["source"]) + self.assertEqual(2, report["updated_devices"]) + self.assertEqual(0, report["created_devices"]) + self.assertEqual(["UD:1", "UD:2"], [device.Label for device in report["devices"]]) + self.assertEqual([110.0, 115.2], [device.Placement.Base.x for device in report["devices"]]) + self.assertEqual(["terminal-ud-1", "terminal-ud-2"], [terminal.QetTerminalUuid for terminal in report["terminals"]]) + self.assertFalse(any(terminal.QetTerminalUuid.startswith("local:") for terminal in report["terminals"])) + self.assertEqual("layout_existing", ud1.QetBatchAssemblyMode) + self.assertEqual("rail", ud1.QetMountHostKind) + + def test_layout_existing_terminal_block_moves_group_children_for_document_groups(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1") + body = doc.addObject("Part::Feature", "TerminalSlice_GreenBody") + body.Placement = app.Placement(app.Vector(10, 20, 30), app.Rotation()) + ud1.addObject(body) + self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1") + + report = batch_assembly.layout_existing_terminal_block( + doc, + rail, + block_name="UD", + pitch_mm=5.2, + start_offset_mm=10.0, + ) + + self.assertEqual(1, report["updated_devices"]) + self.assertNotEqual(10.0, body.Placement.Base.x) + self.assertAlmostEqual(110.0, ud1.Placement.Base.x) + + def test_available_terminal_strip_names_comes_from_existing_qet_devices(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1") + id2 = self._qet_device(doc, terminal_objects, "ID:2", instance_id="id-2", element_uuid="element-id-2") + self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1") + self._terminal(doc, terminal_objects, id2, "terminal-id-2", "ID:2") + + self.assertEqual(["ID", "UD"], batch_assembly.available_terminal_strip_names(doc)) + + def test_layout_existing_devices_filters_qet_breakers_and_ignores_terminal_slices(self): + _install_fake_freecad() + terminal_objects, batch_assembly = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") + terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") + + qf2 = self._qet_device(doc, terminal_objects, "QF2", instance_id="qf-2", element_uuid="element-qf-2") + qf1 = self._qet_device(doc, terminal_objects, "QF1", instance_id="qf-1", element_uuid="element-qf-1") + ta1 = self._qet_device(doc, terminal_objects, "TA1", instance_id="ta-1", element_uuid="element-ta-1") + ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1") + self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1") + qf0 = self._qet_device(doc, terminal_objects, "QF0", instance_id="qf-0", element_uuid="element-qf-0") + terminal_objects.ensure_string_property( + qf0, + "QetBatchAssemblyKind", + "QET Batch Assembly", + "", + "breaker_batch", + ) + + report = batch_assembly.layout_existing_devices( + doc, + rail, + prefix="QF", + pitch_mm=18.0, + start_offset_mm=5.0, + kind="breaker_batch", + ) + + self.assertEqual("qet_existing", report["source"]) + self.assertEqual(["QF1", "QF2"], [device.Label for device in report["devices"]]) + self.assertNotIn(ta1, report["devices"]) + self.assertNotIn(ud1, report["devices"]) + self.assertNotIn(qf0, report["devices"]) + self.assertEqual([5.0, 23.0], [device.Placement.Base.x for device in report["devices"]]) + self.assertEqual("layout_existing", qf1.QetBatchAssemblyMode) + def test_create_terminal_block_places_slices_and_local_terminals_along_selected_rail(self): _install_fake_freecad() terminal_objects, batch_assembly = _reload_modules() diff --git a/tests/python/freecad_exchange_manual_wiring_panel_test.py b/tests/python/freecad_exchange_manual_wiring_panel_test.py index 1d104fc..5ec5884 100644 --- a/tests/python/freecad_exchange_manual_wiring_panel_test.py +++ b/tests/python/freecad_exchange_manual_wiring_panel_test.py @@ -613,6 +613,77 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual(500.0, getattr(carrier, "QetCarrierLength", None)) self.assertEqual(2.5, getattr(carrier, "QetCarrierScaleX", None)) + def test_controller_applies_length_when_selected_object_is_carrier_child(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + carrier = doc.addObject("App::DocumentObjectGroup", "WireDuctCarrier") + child = doc.addObject("Part::Feature", "WireDuctBody") + nested_child = doc.addObject("Part::Feature", "WireDuctBodyFaceOwner") + carrier.addObject(child) + child.addObject(nested_child) + terminal_objects.ensure_string_property( + carrier, + "QetCarrierKind", + "QET Wiring", + "Carrier kind", + "wire_duct", + ) + carrier.addProperty("App::PropertyFloat", "QetCarrierBaseLength", "QET Wiring", "Base length") + carrier.QetCarrierBaseLength = 200.0 + selection_state["selection"] = [nested_child] + + updated = panel.ManualWiringController().apply_length_to_selected_carriers(400.0) + + self.assertEqual([carrier], updated) + self.assertEqual(400.0, getattr(carrier, "QetCarrierLength", None)) + self.assertEqual(2.0, getattr(carrier, "QetCarrierScaleX", None)) + + def test_controller_auto_marks_selected_wire_duct_by_name_before_length_change(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + duct = doc.addObject("Part::Feature", "qet_wire_duct_001") + duct.Label = "线槽001" + selection_state["selection"] = [duct] + + updated = panel.ManualWiringController().apply_length_to_selected_carriers(450.0) + + carrier_group = doc.getObject("QETWiring_02_Carriers") + self.assertEqual([duct], updated) + self.assertEqual("wire_duct", duct.QetCarrierKind) + self.assertEqual("线槽", duct.QetCarrierRoleLabel) + self.assertIn(duct, carrier_group.Group) + self.assertEqual(450.0, duct.QetCarrierLength) + + def test_controller_auto_detects_selected_din_rail_for_batch_placement(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("App::DocumentObjectGroup", "DINRail001") + rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + selection_state["selection"] = [rail] + + report = panel.ManualWiringController().create_breakers_from_selection( + base_name="QF", + count=1, + ) + + self.assertEqual(1, report["created_devices"]) + self.assertEqual("rail", rail.QetCarrierKind) + def test_controller_aligns_second_selected_face_to_first_selected_face(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() From ec0f105c929232f4f6f3427c341d33df52eca615 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Wed, 3 Jun 2026 16:48:41 +0800 Subject: [PATCH 58/63] =?UTF-8?q?feat(freecad):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=B8=BB=E8=B7=AF=E5=BE=84=E7=AB=AF=E7=82=B9=E6=A1=A5=E6=8E=A5?= =?UTF-8?q?=E5=B8=83=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 14 ++-- src/Mod/FreeCADExchange/AutoRouting.py | 4 +- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 2 +- src/Mod/FreeCADExchange/RoutingNetwork.py | 19 ++++-- .../freecad_exchange_auto_routing_test.py | 65 ++++++++++++++++++- 5 files changed, 87 insertions(+), 17 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 808b7e1..080d2bb 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -254,9 +254,9 @@ QET 侧如果能提供端子排/断路器的顺序、数量和显示编号,则 构图时不要求所有 carrier 都提前手工打断。系统会识别轴向线段之间的几何相交和同线重叠,把交点/重叠端点自动切成图节点。这样多条线槽中心路径只要在空间中相交,就可以在交点处换向,Dijkstra 才能得到符合工程布线习惯的折线路径。 -相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝。 +相邻主路径端点允许存在小间隙。默认情况下,`WireDuct` / `UserPath` / `WiringCutOut` 等主路径端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度、线槽端部留缝,以及用户路径贴近线槽但未精确相交的情况。 -FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。面板摘要和检查报告都会按当前容差显示自动桥接段数,便于确认当前容差是否生效。 +FreeCAD 的 `3D 布线连接` 面板提供“主路径桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。内部字段名仍沿用 `adjoining_duct_tolerance` 以兼容已有代码,但界面语义已明确为主路径端点桥接。面板摘要和检查报告都会按当前容差显示自动桥接段数,便于确认当前容差是否生效。 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 @@ -543,7 +543,7 @@ QetWiringCutOutBridgeExtensionMm = 20.0 批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。 -批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻线槽自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 +批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻主路径自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长等问题带出来。 @@ -627,7 +627,7 @@ tests/python/freecad_exchange_auto_routing_test.py 18. “准备布线布局空间”始终按整份文档识别线槽、支撑面和工程端子,并标记障碍处理方式。 19. “生成布线路径网络”按 EPLAN 的 Generate routing path network 语义生成 `WireDuct` / `UserPath` / `WiringCutOut` / `RoutingRange` / `TerminalAccess` carrier;有选择时,选中线槽或草图路径只作为额外识别提示,仍会扫描整份文档。 20. “生成布线连接”会先更新同一套布线路径网络,再按全部 QET 导线任务批量求路。 -21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 +21. 相邻主路径端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 @@ -635,8 +635,8 @@ tests/python/freecad_exchange_auto_routing_test.py 26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 -29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。 -30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 +29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻主路径端点自动桥接容差,并在网络结果中记录桥接段数量。 +30. `3D 布线连接` 面板提供“主路径桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 @@ -663,7 +663,7 @@ tests/manual/freecad_auto_routing_smoke.py 3. 清除布线连接 4. 清除走线路径 5. 点击“准备布线布局空间” -6. 按当前机柜情况调整线槽桥接容差、端子接入最大距离、端子出线长度 +6. 按当前机柜情况调整主路径桥接容差、端子接入最大距离、端子出线长度 7. 可选:选中无法自动识别的线槽实体 8. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别 9. 点击“生成布线连接” diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index ae356d9..4d44eb6 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -2032,7 +2032,7 @@ def format_eplan_connection_route_report(report): blocked_segments = _route_network_metric_max(report, "blocked_segments") network_parts = [] if bridged_segments > 0: - network_parts.append("自动桥接 {0} 段相邻线槽".format(bridged_segments)) + network_parts.append("自动桥接 {0} 段相邻主路径".format(bridged_segments)) if blocked_segments > 0: network_parts.append("避障屏蔽 {0} 段".format(blocked_segments)) if network_parts: @@ -2475,7 +2475,7 @@ def format_routing_path_network_report(diagnostic): ) bridged_segments = int(summary.get("bridged_segments", 0) or 0) if bridged_segments > 0: - message += " 自动桥接 {0} 段相邻线槽。".format(bridged_segments) + message += " 自动桥接 {0} 段相邻主路径。".format(bridged_segments) return message message = "布线路径网络检查发现 {0} 类问题。".format(len(issues)) diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index abbc0dd..f3ff62e 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -289,7 +289,7 @@ class AutoRoutingTaskPanel: layout = QtWidgets.QVBoxLayout(self.form) options_layout = QtWidgets.QHBoxLayout() - options_layout.addWidget(QtWidgets.QLabel("线槽桥接容差 mm")) + options_layout.addWidget(QtWidgets.QLabel("主路径桥接容差 mm")) self.adjoining_duct_tolerance_spin = QtWidgets.QDoubleSpinBox() self.adjoining_duct_tolerance_spin.setRange(0.0, 1000.0) self.adjoining_duct_tolerance_spin.setDecimals(1) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index de8bb26..21b33ed 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -23,6 +23,14 @@ ROUTE_CARRIER_KIND_USER_PATH = "UserPath" ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath" ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange" ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess" +BRIDGEABLE_ENDPOINT_CARRIER_KINDS = { + 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, + ROUTE_CARRIER_KIND_AUXILIARY_PATH, +} MANAGED_ROUTE_SOURCE_KINDS = { ROUTE_CARRIER_KIND_WIRE_DUCT, ROUTE_CARRIER_KIND_WIRING_CUT_OUT, @@ -2707,7 +2715,7 @@ def build_route_graph( bridged_segment_count = 0 blocked_bboxes = list(blocked_bboxes or []) segments = [] - wire_duct_endpoint_nodes = [] + bridgeable_endpoint_nodes = [] def ensure_node(point): key = _point_key(point, tolerance=tolerance) @@ -2762,10 +2770,11 @@ def build_route_graph( if len(ordered) < 2: continue carrier = segment["carrier"] - if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT: + carrier_kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + if carrier_kind in BRIDGEABLE_ENDPOINT_CARRIER_KINDS: for endpoint in (ordered[0], ordered[-1]): endpoint_key = ensure_node(endpoint) - wire_duct_endpoint_nodes.append((endpoint_key, nodes[endpoint_key], carrier)) + bridgeable_endpoint_nodes.append((endpoint_key, nodes[endpoint_key], carrier)) previous_key = ensure_node(ordered[0]) previous_point = nodes[previous_key] for point in ordered[1:]: @@ -2787,9 +2796,9 @@ def build_route_graph( adjoining_limit = max(float(adjoining_duct_tolerance or 0.0), 0.0) bridged_pairs = set() if adjoining_limit > tolerance: - for left_index, left in enumerate(wire_duct_endpoint_nodes): + for left_index, left in enumerate(bridgeable_endpoint_nodes): left_key, left_point, left_carrier = left - for right_key, right_point, right_carrier in wire_duct_endpoint_nodes[left_index + 1:]: + for right_key, right_point, right_carrier in bridgeable_endpoint_nodes[left_index + 1:]: if left_key == right_key or left_carrier is right_carrier: continue pair = tuple(sorted((left_key, right_key))) diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 58e26b0..719f249 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -815,6 +815,34 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) + def test_route_graph_bridges_adjoining_user_path_to_wire_duct_gap(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(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="UserPath", + ) + + network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) + start_key, _start_distance = routing_network.nearest_node(network, app.Vector(0, 0, 20)) + end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) + result = routing_network.shortest_path_with_carriers(network, start_key, end_key) + + self.assertEqual(1, network["bridged_segment_count"]) + self.assertIsNotNone(result) + self.assertIn("UserPath", result["carrier_kinds"]) + def test_auto_routing_respects_adjoining_duct_tolerance_option(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -846,6 +874,39 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("Routed", result["route_status"]) self.assertEqual(1, result["network"]["bridged_segments"]) + def test_auto_routing_uses_bridged_user_path_to_wire_duct_gap(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(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="UserPath", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"adjoining_duct_tolerance": 15.0}, + ) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(1, result["network"]["bridged_segments"]) + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self): _install_fake_freecad() _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -1981,7 +2042,7 @@ class AutoRoutingTest(unittest.TestCase): message = auto_routing.format_routing_path_network_report(diagnostic) - self.assertIn("桥接 1 段", message) + self.assertIn("桥接 1 段相邻主路径", message) def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self): _install_fake_freecad() @@ -3203,7 +3264,7 @@ class AutoRoutingTest(unittest.TestCase): message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径网络:自动桥接 1 段相邻线槽,避障屏蔽 2 段。", message) + self.assertIn("路径网络:自动桥接 1 段相邻主路径,避障屏蔽 2 段。", message) def test_route_report_includes_parallel_lane_summary(self): _install_fake_freecad() From cedd38df40c131d6d418a0385b581f84b8af7edd Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Wed, 3 Jun 2026 16:51:54 +0800 Subject: [PATCH 59/63] =?UTF-8?q?fix(freecad):=20=E5=AE=8C=E5=96=84QET?= =?UTF-8?q?=E7=AB=AF=E5=AD=90=E6=89=B9=E9=87=8F=E8=A3=85=E9=85=8D=E7=BB=91?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06-02-batch-din-device-placement-design.md | 18 ++++- src/Mod/FreeCADExchange/BatchAssembly.py | 20 ++++- src/Mod/FreeCADExchange/TerminalImport.py | 58 +++++++++++++ .../freecad_exchange_batch_assembly_test.py | 13 ++- ...nge_terminal_import_template_slots_test.py | 81 +++++++++++++++++++ .../freecad_exchange_wiring_import_test.py | 12 +-- 6 files changed, 191 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md index 9e54fef..d094f53 100644 --- a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md +++ b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md @@ -21,6 +21,8 @@ QET 负责: - 设备与 3D 模型资产绑定。 - 端子的真实 `terminal_uuid`。 +当前交换 JSON 中,正式端子优先来自 `devices[].terminals[]`。顶层 `terminals[]` 可以为空,不能因此判断 QET 没有传端子。导线任务的 `start_terminal_uuid / end_terminal_uuid` 也使用同一套真实端子 UUID。 + FreeCAD 负责: - 真实 3D 设备实例的空间位姿。 @@ -31,6 +33,18 @@ FreeCAD 负责: 第一版仍遵守 2D/3D 协同约束:3D 端子绑定唯一依据是 `terminal_uuid`,3D 位姿以 `scene.FCStd` 为准,不从数据库反推 3D 位姿。 +## 端子导入顺序 + +FreeCAD 导入工程端子时按下面顺序读取: + +```text +1. 顶层 terminals[] +2. devices[].terminals[] +3. wires[] 中的起点/终点端子,仅作为缺失端子的兜底补齐 +``` + +如果 `devices[].terminals[]` 已经包含某个导线端点,`wires[]` 不会重复生成同一个端子。正式工程中生成的工程端子必须保留 QET 传入的 `terminal_uuid`,包括 `element_uuid:terminal_uuid` 这种复合字符串,不允许转换为 `local:*`。 + ## 端子排批量排布 正式流程: @@ -80,6 +94,8 @@ FreeCAD 负责: 8. 按自然顺序排布,例如 `QF1`、`QF2`、`QF10`。 9. 保留设备下的工程端子和 QET 绑定关系。 +断路器筛选只处理真实设备对象,不处理设备下的工程端子对象或 `QET Terminals` 分组。工程端子和端子分组也会携带 `QetInstanceId / QetElementUuid`,不能只按这些字段判断为设备,否则 `QF1:1` 这类端子会被误当成断路器一起排布。 + 断路器端子号来自 QET 传入的真实端子数据。参数窗口中的“兜底端子号”只在当前工程没有匹配 QET 设备、需要演示生成占位对象时使用。 ## 旧兜底逻辑 @@ -159,7 +175,7 @@ QET模板 -> 3D手动布线 2. 树目录中已经存在 QET 导入的端子片或设备实例。 3. 选中导轨,点击 `批量端子排`。 4. 输入 `UD` 或 `ID`,确认后真实端子片沿导轨排布。 -5. 排布后端子对象仍保留真实 `QetTerminalUuid`,不会变成 `local:*`。 +5. 排布后端子对象仍保留真实 `QetTerminalUuid`,包括 `element_uuid:terminal_uuid` 这种 QET -> FreeCAD 交换身份,不会变成 `local:*`。 6. 选中导轨,点击 `批量断路器`。 7. 输入 `QF`,确认后真实断路器沿导轨排布。 8. 保存后重新打开 `scene.FCStd`,设备位置保持。 diff --git a/src/Mod/FreeCADExchange/BatchAssembly.py b/src/Mod/FreeCADExchange/BatchAssembly.py index 2be0012..0f26efc 100644 --- a/src/Mod/FreeCADExchange/BatchAssembly.py +++ b/src/Mod/FreeCADExchange/BatchAssembly.py @@ -75,6 +75,15 @@ def _text_values(obj, include_children=False): return values +def _label_name_values(obj): + values = [] + for attr_name in ("Label", "Name"): + value = (getattr(obj, attr_name, "") or "").strip() + if value: + values.append(value) + return values + + def _natural_sort_key(value): text = str(value or "") key = [] @@ -96,7 +105,7 @@ def _parse_strip_name_and_order(obj): order = _order_from_texts(_text_values(obj)) return strip_name, order - for text in _text_values(obj): + for text in _label_name_values(obj): # Examples from QET trees: UD:1, UD-2, ID_006. match = re.match(r"^\s*([A-Za-z][A-Za-z0-9]{0,8})\s*[::_\-]\s*(\d+)\b", text) if match: @@ -139,10 +148,19 @@ def _qet_identity(obj): def _is_qet_device_object(obj): if obj is None: return False + if TerminalObjects.is_terminal_object(obj): + return False + group_kind = (getattr(obj, "QetGroupKind", "") or "").strip() + if group_kind in {TerminalObjects.TERMINAL_GROUP_KIND, TerminalObjects.WIRE_GROUP_KIND}: + return False name = getattr(obj, "Name", "") or "" + if name.startswith(TerminalObjects.TERMINAL_GROUP_PREFIX) or name.startswith(TerminalObjects.WIRE_GROUP_PREFIX): + return False instance_id, element_uuid = _qet_identity(obj) if name.startswith(TerminalObjects.DEVICE_GROUP_PREFIX): return True + if group_kind == "Device": + return True return bool(instance_id or element_uuid) diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index 88bed6e..8b922a2 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -93,6 +93,56 @@ def _payload_device_instance_by_element(payload): return result +def _device_embedded_terminal_entries(payload, existing_keys): + devices = payload.get("devices", []) or [] + if not isinstance(devices, list): + return [] + + seen = set(existing_keys or set()) + entries = [] + for device in devices: + 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_terminals = device.get("terminals", []) or [] + if not isinstance(device_terminals, list): + continue + + for terminal in device_terminals: + 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): + continue + + # QET 的正式端子可能直接挂在 devices[].terminals[] 下。 + # 直接调用本模块时也要读取它,避免正式布线匹配退回 local:* 端子。 + key = (element_uuid, terminal_uuid) + if key in seen: + continue + seen.add(key) + terminal_display = ( + terminal.get("terminal_display") + or terminal.get("terminal_label") + or terminal.get("slot_name") + or "" + ) + entries.append( + { + "terminal_uuid": terminal_uuid, + "element_uuid": element_uuid, + "instance_id": instance_id, + "terminal_display": terminal_display, + "slot_name_hint": terminal_display, + } + ) + return entries + + def _wire_endpoint_terminal_entries(payload, existing_keys): wires = payload.get("wires", []) or [] if not isinstance(wires, list): @@ -382,6 +432,13 @@ def import_terminals_from_payload(payload, scene_path=""): terminal_uuid = (item.get("terminal_uuid") or "").strip() if element_uuid and terminal_uuid: terminal_entry_keys.add((element_uuid, terminal_uuid)) + embedded_entries = _device_embedded_terminal_entries(payload, terminal_entry_keys) + terminal_entries.extend(embedded_entries) + terminal_entry_keys.update( + (entry["element_uuid"], entry["terminal_uuid"]) + for entry in embedded_entries + if entry.get("element_uuid") and entry.get("terminal_uuid") + ) synthesized_entries = _wire_endpoint_terminal_entries(payload, terminal_entry_keys) terminal_entries.extend(synthesized_entries) @@ -398,6 +455,7 @@ def import_terminals_from_payload(payload, scene_path=""): "reused_template_hints": 0, "matched_by_slot_hint": 0, "generated_fallback_slots": 0, + "device_embedded_terminals": len(embedded_entries), "synthesized_wire_endpoint_terminals": len(synthesized_entries), "skipped_missing_slot": 0, "skipped_missing_device": 0, diff --git a/tests/python/freecad_exchange_batch_assembly_test.py b/tests/python/freecad_exchange_batch_assembly_test.py index 123ab60..46ddfaa 100644 --- a/tests/python/freecad_exchange_batch_assembly_test.py +++ b/tests/python/freecad_exchange_batch_assembly_test.py @@ -181,8 +181,8 @@ class BatchAssemblyTest(unittest.TestCase): ud2 = self._qet_device(doc, terminal_objects, "UD:2", instance_id="ud-2", element_uuid="element-ud-2") ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1") - self._terminal(doc, terminal_objects, ud2, "terminal-ud-2", "UD:2") - self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1") + self._terminal(doc, terminal_objects, ud2, "element-ud-2:terminal-template-1", "UD:2") + self._terminal(doc, terminal_objects, ud1, "element-ud-1:terminal-template-1", "UD:1") report = batch_assembly.layout_existing_terminal_block( doc, @@ -197,7 +197,10 @@ class BatchAssemblyTest(unittest.TestCase): self.assertEqual(0, report["created_devices"]) self.assertEqual(["UD:1", "UD:2"], [device.Label for device in report["devices"]]) self.assertEqual([110.0, 115.2], [device.Placement.Base.x for device in report["devices"]]) - self.assertEqual(["terminal-ud-1", "terminal-ud-2"], [terminal.QetTerminalUuid for terminal in report["terminals"]]) + self.assertEqual( + ["element-ud-1:terminal-template-1", "element-ud-2:terminal-template-1"], + [terminal.QetTerminalUuid for terminal in report["terminals"]], + ) self.assertFalse(any(terminal.QetTerminalUuid.startswith("local:") for terminal in report["terminals"])) self.assertEqual("layout_existing", ud1.QetBatchAssemblyMode) self.assertEqual("rail", ud1.QetMountHostKind) @@ -255,6 +258,8 @@ class BatchAssemblyTest(unittest.TestCase): qf2 = self._qet_device(doc, terminal_objects, "QF2", instance_id="qf-2", element_uuid="element-qf-2") qf1 = self._qet_device(doc, terminal_objects, "QF1", instance_id="qf-1", element_uuid="element-qf-1") + qf2_terminal = self._terminal(doc, terminal_objects, qf2, "terminal-qf-2-1", "QF2:1") + qf1_terminal = self._terminal(doc, terminal_objects, qf1, "terminal-qf-1-1", "QF1:1") ta1 = self._qet_device(doc, terminal_objects, "TA1", instance_id="ta-1", element_uuid="element-ta-1") ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1") self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1") @@ -281,6 +286,8 @@ class BatchAssemblyTest(unittest.TestCase): self.assertNotIn(ta1, report["devices"]) self.assertNotIn(ud1, report["devices"]) self.assertNotIn(qf0, report["devices"]) + self.assertNotIn(qf1_terminal, report["devices"]) + self.assertNotIn(qf2_terminal, report["devices"]) self.assertEqual([5.0, 23.0], [device.Placement.Base.x for device in report["devices"]]) self.assertEqual("layout_existing", qf1.QetBatchAssemblyMode) 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 d6e09b1..0b7438e 100644 --- a/tests/python/freecad_exchange_terminal_import_template_slots_test.py +++ b/tests/python/freecad_exchange_terminal_import_template_slots_test.py @@ -350,6 +350,87 @@ class TerminalImportTemplateSlotPolicyTest(unittest.TestCase): self.assertEqual("terminal-b", end_terminals[0].QetTerminalUuid) self.assertEqual("device-b", end_terminals[0].QetElementUuid) + def test_import_reads_qet_terminals_embedded_in_devices(self): + _install_fake_freecad() + terminal_import, terminal_objects, device_import = _reload_modules() + + doc = FakeDocument() + device_import._ensure_document = lambda scene_path: doc + root = device_import._ensure_root_group(doc, project_uuid="project-1") + device = doc.addObject("App::Part", "QETDevice_device_a") + root.addObject(device) + terminal_objects.ensure_string_property( + device, + "QetProjectUuid", + "QET Exchange", + "Project UUID", + "project-1", + ) + terminal_objects.ensure_string_property( + device, + "QetElementUuid", + "QET Exchange", + "Element UUID", + "device-a", + ) + terminal_objects.ensure_string_property( + device, + "QetInstanceId", + "QET Exchange", + "Instance ID", + "instance-a", + ) + + report = terminal_import.import_terminals_from_payload( + { + "project_uuid": "project-1", + "devices": [ + { + "element_uuid": "device-a", + "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", + } + ], + } + ) + + terminal_group = terminal_objects.find_child_group_by_kind( + device, + terminal_objects.TERMINAL_GROUP_KIND, + ) + 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["synthesized_wire_endpoint_terminals"]) + self.assertEqual(1, len(terminals)) + self.assertEqual("device-a:terminal-p1", terminals[0].QetTerminalUuid) + self.assertEqual("device-a", terminals[0].QetElementUuid) + self.assertEqual("instance-a", terminals[0].QetInstanceId) + self.assertEqual("P1", terminals[0].Label) + self.assertFalse(terminals[0].QetTerminalUuid.startswith("local:")) + def test_import_prefers_terminal_element_uuid_over_conflicting_instance_id(self): _install_fake_freecad() terminal_import, terminal_objects, device_import = _reload_modules() diff --git a/tests/python/freecad_exchange_wiring_import_test.py b/tests/python/freecad_exchange_wiring_import_test.py index d63ac4c..3b1980c 100644 --- a/tests/python/freecad_exchange_wiring_import_test.py +++ b/tests/python/freecad_exchange_wiring_import_test.py @@ -124,11 +124,11 @@ class WiringImportTest(unittest.TestCase): "group_uuid": "group-1", "start_element_uuid": "device-a", "start_instance_id": "instance-a", - "start_terminal_uuid": "terminal-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": "terminal-b", + "end_terminal_uuid": "device-b:terminal-b", "end_terminal_display": "B1", } ], @@ -164,9 +164,9 @@ class WiringImportTest(unittest.TestCase): "wire_mark": "W001", "wire_mark_is_manual": True, "start_element_uuid": "device-a", - "start_terminal_uuid": "terminal-a", + "start_terminal_uuid": "device-a:terminal-a", "end_element_uuid": "device-b", - "end_terminal_uuid": "terminal-b", + "end_terminal_uuid": "device-b:terminal-b", "start_terminal_display": "A1", "end_terminal_display": "B1", "conductor_uuids": ["conductor-1"], @@ -187,8 +187,8 @@ class WiringImportTest(unittest.TestCase): self.assertEqual("group-1", task.QetGroupUuid) self.assertEqual("W001", task.QetWireMark) self.assertTrue(task.QetWireMarkIsManual) - self.assertEqual("terminal-a", task.QetStartTerminalUuid) - self.assertEqual("terminal-b", task.QetEndTerminalUuid) + self.assertEqual("device-a:terminal-a", task.QetStartTerminalUuid) + self.assertEqual("device-b:terminal-b", task.QetEndTerminalUuid) self.assertEqual("device-a", task.QetStartElementUuid) self.assertEqual("device-b", task.QetEndElementUuid) self.assertEqual("A1", task.QetStartTerminalDisplay) From 199412b2c8ae9185ee6a464b89d06438ad8ea49c Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Thu, 4 Jun 2026 17:53:32 +0800 Subject: [PATCH 60/63] =?UTF-8?q?feat(freecad):=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84=E8=AF=8A?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 74 +- src/Mod/FreeCADExchange/AutoRouting.py | 533 +++++++++-- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 25 + src/Mod/FreeCADExchange/RoutingNetwork.py | 321 +++++-- .../freecad_exchange_auto_routing_test.py | 847 +++++++++++++++++- 5 files changed, 1670 insertions(+), 130 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 080d2bb..1307203 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -254,15 +254,17 @@ QET 侧如果能提供端子排/断路器的顺序、数量和显示编号,则 构图时不要求所有 carrier 都提前手工打断。系统会识别轴向线段之间的几何相交和同线重叠,把交点/重叠端点自动切成图节点。这样多条线槽中心路径只要在空间中相交,就可以在交点处换向,Dijkstra 才能得到符合工程布线习惯的折线路径。 -相邻主路径端点允许存在小间隙。默认情况下,`WireDuct` / `UserPath` / `WiringCutOut` 等主路径端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度、线槽端部留缝,以及用户路径贴近线槽但未精确相交的情况。 +相邻主路径允许存在小间隙。默认情况下,`WireDuct` / `UserPath` / `WiringCutOut` 等主路径端点距离不超过 5 mm 时会被视为相邻并自动桥接;如果支路端点靠近另一条主路径的中段,也会把端点投影到该主路径中段并补一条虚拟桥接边。桥接段仍会经过障碍包围盒检查,穿过障碍时不会建立桥接。自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度、线槽端部留缝,以及用户路径贴近线槽但未精确相交的情况。 -FreeCAD 的 `3D 布线连接` 面板提供“主路径桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。内部字段名仍沿用 `adjoining_duct_tolerance` 以兼容已有代码,但界面语义已明确为主路径端点桥接。面板摘要和检查报告都会按当前容差显示自动桥接段数,便于确认当前容差是否生效。 +FreeCAD 的 `3D 布线连接` 面板提供“主路径桥接容差 mm”数值框,手动测试时可直接调整这个选项;生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。内部字段名仍沿用 `adjoining_duct_tolerance` 以兼容已有代码,但界面语义已明确为主路径端点桥接和端点到中段投影桥接。面板摘要和检查报告都会按当前容差显示自动桥接段数,便于确认当前容差是否生效。 同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。 +`TerminalAccess` 定位为端子局部接入线,只用于把端子出口引到柜内主路径附近。最终导线的主路径搜索不会把 `TerminalAccess` 当作公共 transit carrier,也不会用它桥接两段线槽或 `UserPath` 的缺口;入口候选排序也会优先选择线槽、`UserPath`、过线孔等真实主路径,避免导线贴到其它端子的局部接入线上起步。这类缺口应通过线槽、`UserPath`、过线孔或主路径自动桥接来解决。 + 路径网络检查会诊断异常长的 `TerminalAccess`。当端子接入段明显过长时,报告会提示“端子接入过长”,建议补设备局部路径、移动设备,或补一段 `UserPath` / 线槽靠近端子。这类诊断用于避免设备未摆放好时生成看起来悬空或穿越设备区域的接入线。 -面板还提供“并行线间距 mm”和“并行线方向”,用于控制多根导线共用同一路径时的可视 lane 偏移。方向默认 `auto`,也可以手动指定 `x`、`y`、`z`。这些设置只影响 3D 显示上导线之间的错位方式,不代表真实线槽截面内的排布位置。 +面板还提供“并行线间距 mm”、“并行线最大偏移 mm”和“并行线方向”,用于控制多根导线共用同一路径时的可视 lane 偏移。方向默认 `auto`,也可以手动指定 `x`、`y`、`z`。最大偏移用于限制密集共路时的显示错位范围,避免 lane 序号过大时把导线显示到线槽或柜体外。这些设置只影响 3D 显示上导线之间的错位方式,不代表真实线槽截面内的排布位置。 ### 2.1 路由优先级 @@ -452,7 +454,7 @@ terminal_exit_length = 20.0 terminal_access_max_distance = 1000.0 ``` -`terminal_exit_length` 决定端子出线段长度;`terminal_access_max_distance` 决定端子出线点到最近路由网络的最大允许接入距离。两个参数都只保存在当前 FreeCAD 面板/调用选项中,不写数据库。 +`terminal_exit_length` 决定端子出线段长度;`terminal_access_max_distance` 决定端子出线点到最近路由网络的最大允许接入距离,并同时约束最终导线路由入口候选,避免用户调小端子接入距离后,最终求路仍跨很远接入孤立网络。两个参数都只保存在当前 FreeCAD 面板/调用选项中,不写数据库。 网络检查发现端子未接入时,诊断 JSON 会记录该端子到最近路由网络的距离、当前端子接入最大距离和端子出线长度;面板报告会显示当前最大接入距离,便于判断是设备/线槽位置还没摆好,还是需要临时调大接入阈值。 @@ -541,21 +543,29 @@ QetWiringCutOutBridgeExtensionMm = 20.0 生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象,route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。route track 同时记录 carrier 的 `capacity`,用于后续核对多根线共路、容量偏好和绕行行为。 -批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。 +如果导线实际走过自动桥接边,`QetRouteTrackJson` 中对应段会记录 `is_bridge=true`,并汇总 `bridged_segments`。批量布线报告和诊断对象中的 route sample 会优先使用这个本路线实际桥接数量;旧诊断缺少该字段时,再回退到整张路径网络的桥接数量。自动桥接段是虚拟连通边,不代表真实线槽截面,因此不参与容量最小值计算,也不参与共路 lane 计数、路径复用惩罚、真实 carrier 类型汇总、诊断样例 carrier 列表和路径质量提示。 + +批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。路径示例会跳过 `is_bridge=true` 的虚拟桥接段,避免把自动补出来的连通边误显示成真实线槽或用户路径。 -批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻主路径自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 +批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻/投影主路径自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长等问题带出来。 +当导线因为缺少布线路径网络被跳过时,批量报告会显示一条“缺路径网络示例”,包含导线号、起终点端子标签和已记录的失败原因。这里既包括整份文档没有有效路径段,也包括路径网络存在但该导线两端无法连通、端子接入距离阈值过小等情况。手动测试时可先按该示例定位设备两端附近是否缺线槽、`UserPath`、过线孔或布线面路径,或判断是否需要调整端子接入距离。 + +当最终导线虽然布通、但起点或终点到主路径入口距离超过警戒阈值时,批量报告会显示“接入距离提示”,列出触发导线数量、一条导线样例及起点/终点接入距离。这个提示不阻止生成导线,用于暴露设备附近缺少局部路径、主路径离端子过远或端子接入距离设置过大的情况。批量诊断 JSON 也会记录 `route_entry_distance_warning_count` 和 `route_entry_distance_warning_samples`,便于导出后定位全部样例。 + 当单条路线使用 `RoutingRange` 或 `AuxiliaryPath` 时,批量报告会提示“路径质量提示”,说明该导线可能没有完全优先进入线槽。这个提示不阻止布线,只用于暴露“当前路径依赖布线面兜底”的情况,方便后续补线槽、补 `UserPath` 或调整设备位置。批量诊断 JSON 也会记录这类提示:`route_quality_warning_count` 表示依赖布线面/辅助路径的导线数量,`route_quality_warning_samples` 保留少量导线样例及其使用的 carrier 类型。 路径网络检查还会识别“只有 `RoutingRange`、没有 `WireDuct` / `UserPath` / `WiringCutOut` 主路径”的情况,并记录 `routing_range_only_network`。这类网络可以作为无线槽或路径不完整时的临时兜底,但不是推荐的第一版主路径形态;手动测试看到该提示时,优先补线槽、补 `UserPath` 或补过线孔路径。 如果当前文档没有任何可用路径段,路径网络检查会记录 `empty_routing_path_network`,中文报告显示“布线路径网络为空”。这表示还没有生成可供自动布线搜索的线槽、`UserPath`、过线孔或布线面路径,不能把 0 carrier / 0 segment 当作检查通过。 +端子未接入、端子接入过长和端子局部路径无效等诊断会在中文报告里优先显示 FreeCAD 对象名,并同时保留 `terminal_uuid`。手动测试时可以按对象名在树里定位具体端子,再根据距离提示补设备局部路径、移动设备、补 `UserPath` 或补线槽。 + 如果 carrier 对象存在但 `Points` 为空、只有一个点,或多个点归一化后仍不足两个有效点,路径网络检查会记录 `invalid_route_carriers`,中文报告提示“路径对象几何无效”。这通常意味着用户路径、线槽路径或刷新后的 carrier 几何已经损坏,需要重新生成该路径对象。 -如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号和 lane 间距。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。 +如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号、lane 间距和最大偏移。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位,并能确认密集共路时偏移上限是否参与显示;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。 当单条路线的最大并行线数超过该路线 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。 @@ -627,7 +637,7 @@ tests/python/freecad_exchange_auto_routing_test.py 18. “准备布线布局空间”始终按整份文档识别线槽、支撑面和工程端子,并标记障碍处理方式。 19. “生成布线路径网络”按 EPLAN 的 Generate routing path network 语义生成 `WireDuct` / `UserPath` / `WiringCutOut` / `RoutingRange` / `TerminalAccess` carrier;有选择时,选中线槽或草图路径只作为额外识别提示,仍会扫描整份文档。 20. “生成布线连接”会先更新同一套布线路径网络,再按全部 QET 导线任务批量求路。 -21. 相邻主路径端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 +21. 相邻主路径端点在容差内会被网络自动连通;支路端点靠近主路径中段时也会投影桥接;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 @@ -635,15 +645,28 @@ tests/python/freecad_exchange_auto_routing_test.py 26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。 28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。 -29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻主路径端点自动桥接容差,并在网络结果中记录桥接段数量。 +29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻主路径端点桥接和端点到中段投影桥接容差,并在网络结果中记录桥接段数量。 30. `3D 布线连接` 面板提供“主路径桥接容差 mm”设置,面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。 31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置,用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。 32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。 33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。 -34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。 +34. 批量布线报告会显示最大 lane 编号、lane 间距和最大偏移,方便确认多根线共路时是否发生了可视错位,以及偏移上限是否参与显示。 35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 36. 批量布线报告会在最大并行线数超过路径最小容量时显示容量提示,但当前仍不做真实填充率计算。 -37. `3D 布线连接` 面板提供“并行线间距 mm”和“并行线方向”设置,用于调整多线共路时的可视 lane 偏移。 +37. `3D 布线连接` 面板提供“并行线间距 mm”、“并行线最大偏移 mm”和“并行线方向”设置,用于调整多线共路时的可视 lane 偏移。 +38. 最终导线选路会在多个入口候选中避开接入段穿障碍的入口,并优先选择可避障的线槽 / `UserPath` 入口。 +39. 同一入口下的端子接入正交折线会尝试不同轴向顺序,优先选择不穿过障碍包围盒的折线。 +40. 并行导线可视 lane 偏移默认限制在固定上限内,防止密集共路时导线被显示到柜外。 +41. 完整自动布线流程会使用支路端点到主路径中段的投影桥接,避免这类支路网络被误判为孤立。 +42. `QetRouteTrackJson` 会标记实际走过的自动桥接段,并记录本路线实际使用的桥接段数量。 +43. 批量布线报告的路径示例会跳过虚拟桥接段,只列出真实经过的源对象标签。 +44. 共路 lane 计数和路径复用惩罚会跳过虚拟桥接段,避免仅共享自动桥接边的导线被误判为真实共路。 +45. 路径质量提示会按非桥接段重新判断 carrier 类型,避免把虚拟桥接到 `RoutingRange` 误报为真实使用布线面兜底。 +46. 缺少布线路径网络或路径网络两端不连通时,批量布线报告会显示一条导线、端点样例和失败原因,便于直接定位需要补路径的设备区域。 +47. “端子接入最大距离”同时约束自动 `TerminalAccess` 和最终导线入口候选,防止最终求路绕过面板设置生成超长接入线。 +48. 批量诊断 JSON 的 route sample 会跳过虚拟桥接段统计 carrier 类型和 carrier 名称,保持与中文报告一致。 +49. 最终导线路由不会把 `TerminalAccess` 当作公共 transit carrier,入口候选也会优先真实主路径,避免端子局部接入线被误用来桥接主路径缺口或作为其它导线的起步路径。 +50. 批量布线报告会提示最终导线起点/终点接入距离过长的样例,用于排查设备附近缺局部路径或主路径离端子太远。 已完成 FreeCAD smoke: @@ -809,6 +832,35 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) ## 8. 后续需要完成 +### 8.0 2026-06-03 手动测试问题记录 + +本次手动测试视频 `D:\video\0603布线效果.mp4` 中,真实机柜、导轨、线槽和设备已经装配,但自动布线结果暴露出以下问题: + +1. 80 条 QET 导线任务中只成功生成 43 条,7 条存在碰撞告警,37 条失败。端子匹配没有失败,问题主要出在路径网络连通性和端子接入。 +2. 路径网络规模较大,但检查提示存在孤立路径网络和端子接入过长,说明部分设备局部路径没有可靠接入柜内主路径。 +3. 部分导线穿过设备模型。当前碰撞检测只给出告警,不会强制阻止生成,因此在可用绕行路径不足时仍可能生成穿模导线。 +4. 多根导线在公共路径上共线或高度拥挤。在线槽内共路可以接受,但在线槽外和设备端子附近需要更好的并行错位、束线显示和容量策略。 +5. 部分导线跑到机柜外侧。该问题需要后续增加柜内有效区域/柜体边界诊断,但当前优先级低于提升布通率。 +6. Draft 线段可能悬空。原因通常是 Draft 当前工作平面没有锁定到安装板或线槽面;作为自由空间 `UserPath` 这是允许的,但作为贴面主路径时需要投影、吸附或明确提示。 + +当前开发优先级调整为: + +1. 先保证更多导线能稳定布通,优先处理孤立路径网络和端子接入过长。 +2. 其次降低明显穿模和线槽外共线拥挤。 +3. 柜内越界诊断放到后续阶段,不阻塞当前布通率改进。 + +已完成的对应改进: + +1. 最终导线选路不再只接入最近路径段;当最近路径属于孤立网络且稍远处存在可连通主路径时,会尝试多个接入候选并选择综合成本更低的可连通路径。 +2. 自动生成 `TerminalAccess` 时,不再盲目接入最近的孤立短段;会优先接入更大的连通主网络。 +3. 当线槽 / `UserPath` / 过线孔等主路径存在时,`TerminalAccess` 会优先接入主路径组件;`RoutingRange` 仍保留为无线槽或主路径不足时的兜底布线面。 +4. 最终导线路由进一步提高 `RoutingRange` 的默认成本,避免布线面在有线槽 / `UserPath` 可用时抢占主路径;极短局部过渡和无线槽兜底场景仍可使用 `RoutingRange`。 +5. 最终导线接入候选不再先按几何距离截断;会先按“主路径优先、连通组件优先、距离次之”的规则排序,再取候选,避免大量 `RoutingRange` 网格把稍远的线槽 / `UserPath` 挤出候选列表。 +6. 批量布线报告会补充“接入候选”提示,帮助手动测试判断某条导线是否因为近处孤立网络或布线面网格太密而使用了第几个候选接入口。 +7. 接入候选评分会检查端子出口到路径网络入口之间的小段是否穿过障碍包围盒;当近入口接入段穿模、稍远入口可避开障碍时,会优先选择不穿模的入口。最终碰撞诊断仍保留端点附近设备外壳的宽容规则,避免把端子自身外壳误报成碰撞。 +8. 同一个路径入口已经确定后,端子出口到入口、主路径出口到端子入口的正交折线会尝试不同轴向顺序;当“先走 X”会穿设备、“先走 Y/Z”可绕开时,优先使用不穿模的折线顺序。 +9. 并行导线 lane 偏移增加默认上限,避免大量导线共路时可视错位距离随 lane 序号无限增大,把导线推到线槽或柜体外。lane 序号仍保留,用于容量提示和并行数量报告。 + ### 8.1 近期优先级 1. 线槽语义库 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 4d44eb6..f203606 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -5,6 +5,7 @@ # 然后在 QETWiring_04_Routed 下生成一条可见的折线导线。 import json +import itertools import math import FreeCAD as App @@ -25,10 +26,15 @@ DEFAULT_OPTIONS = { "terminal_exit_length": 20.0, "lane_axis": "auto", "lane_spacing": 10.0, + "lane_max_offset": 30.0, "segment_reuse_penalty": 200.0, # 线槽网络相关参数。 "use_routing_network": True, "network_entry_max_distance": 1000.0, + "network_entry_candidate_limit": 8, + "network_entry_distance_cost_factor": 5.0, + "route_candidate_collision_penalty": 10000.0, + "ignore_endpoint_near_obstacles": True, "adjoining_duct_tolerance": RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, "bend_penalty": 25.0, # EPLAN/SOLIDWORKS 风格:线槽/路由路径最优先,辅助面域只作为过渡/兜底区域。 @@ -40,7 +46,7 @@ DEFAULT_OPTIONS = { "UserPath": 1.0, "AuxiliaryPath": 2.0, "TerminalAccess": 2.0, - "RoutingRange": 25.0, + "RoutingRange": 40.0, }, # 主干必须走 carrier/贴面网络;没有布线路径网络时直接失败。 # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 @@ -176,10 +182,15 @@ def _lane_payload(route_index, options, route_points=None): 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 { "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, } @@ -195,11 +206,7 @@ def _apply_lane_offset(points, lane): ] -def _orthogonal_points(start_point, end_point, preferred_axis=None): - if _vector_close(start_point, end_point): - return [start_point] - - # 每一段只沿一个坐标轴移动,这样生成的线天然是机柜布线常见的折线。 +def _orthogonal_axis_order(start_point, end_point, preferred_axis=None): axis_order = sorted( ("x", "y", "z"), key=lambda axis: abs(_axis_value(end_point, axis) - _axis_value(start_point, axis)), @@ -208,7 +215,10 @@ def _orthogonal_points(start_point, end_point, preferred_axis=None): if preferred_axis in {"x", "y", "z"}: axis_order = [axis for axis in axis_order if axis != preferred_axis] axis_order.append(preferred_axis) + return axis_order + +def _orthogonal_points_for_axis_order(start_point, end_point, axis_order): points = [start_point] current = start_point for axis in axis_order: @@ -221,11 +231,85 @@ def _orthogonal_points(start_point, end_point, preferred_axis=None): return points -def _append_orthogonal(points, target_point, preferred_axis=None): +def _orthogonal_points(start_point, end_point, preferred_axis=None): + if _vector_close(start_point, end_point): + return [start_point] + + # 每一段只沿一个坐标轴移动,这样生成的线天然是机柜布线常见的折线。 + return _orthogonal_points_for_axis_order( + start_point, + end_point, + _orthogonal_axis_order(start_point, end_point, preferred_axis), + ) + + +def _orthogonal_hit_count(points, obstacle_bboxes): + hits = 0 + if not obstacle_bboxes: + return hits + for index in range(max(len(points or []) - 1, 0)): + start = points[index] + end = points[index + 1] + for bbox in obstacle_bboxes: + if _segment_intersects_bbox(start, end, bbox): + hits += 1 + break + return hits + + +def _orthogonal_points_avoiding_obstacles(start_point, end_point, obstacle_bboxes, preferred_axis=None): + base_order = _orthogonal_axis_order(start_point, end_point, preferred_axis) + base_points = _orthogonal_points_for_axis_order(start_point, end_point, base_order) + if not obstacle_bboxes or len(base_order) <= 1: + return base_points + + active_axes = [ + axis + for axis in base_order + if abs(_axis_value(end_point, axis) - _axis_value(start_point, axis)) > 0.000001 + ] + if len(active_axes) <= 1: + return base_points + + inactive_axes = [axis for axis in base_order if axis not in active_axes] + best_points = base_points + best_hits = _orthogonal_hit_count(base_points, obstacle_bboxes) + if best_hits <= 0: + return best_points + + # 同一入口下,“先走 X”或“先走 Y/Z”可能决定端子接入段是否穿模。 + # 这里只重排正交轴顺序,不改变主路径网络和端点绑定语义。 + for order in itertools.permutations(active_axes): + candidate_order = list(order) + inactive_axes + if candidate_order == base_order: + continue + candidate_points = _orthogonal_points_for_axis_order( + start_point, + end_point, + candidate_order, + ) + candidate_hits = _orthogonal_hit_count(candidate_points, obstacle_bboxes) + if candidate_hits < best_hits: + best_hits = candidate_hits + best_points = candidate_points + if best_hits <= 0: + break + return best_points + + +def _append_orthogonal(points, target_point, preferred_axis=None, obstacle_bboxes=None): if not points: _append_unique(points, target_point) return - segment = _orthogonal_points(points[-1], _vector(target_point), preferred_axis) + if obstacle_bboxes: + segment = _orthogonal_points_avoiding_obstacles( + points[-1], + _vector(target_point), + obstacle_bboxes, + preferred_axis=preferred_axis, + ) + else: + segment = _orthogonal_points(points[-1], _vector(target_point), preferred_axis) for point in segment[1:]: _append_unique(points, point) @@ -775,22 +859,31 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non start_exit = start_access_points[-1] if start_access_points else _offset(start_origin, _terminal_direction(start_terminal), exit_length) end_exit = end_access_points[-1] if end_access_points else _offset(end_origin, _terminal_direction(end_terminal), exit_length) - def route_on_network(network, obstacle_aware=False): - if network.get("segment_count", 0) <= 0: - return None - - start_key, start_distance, start_mode = RoutingNetwork.connect_point_to_network(network, start_exit) - end_key, end_distance, end_mode = RoutingNetwork.connect_point_to_network(network, end_exit) - if start_key is None or end_key is None: - return None - - max_distance = float(opts.get("network_entry_max_distance", 0.0) or 0.0) - if max_distance > 0.0 and ( - float(start_distance or 0.0) > max_distance - or float(end_distance or 0.0) > max_distance - ): + def clone_route_network(source): + cloned = dict(source or {}) + cloned["nodes"] = dict((source or {}).get("nodes", {}) or {}) + cloned["edges"] = { + key: list(value or []) + for key, value in ((source or {}).get("edges", {}) or {}).items() + } + cloned["carriers"] = list((source or {}).get("carriers", []) or []) + cloned["bridge_pairs"] = set((source or {}).get("bridge_pairs", set()) or set()) + return cloned + + def build_route_payload( + network, + start_key, + end_key, + start_distance, + end_distance, + start_mode, + end_mode, + obstacle_aware=False, + start_candidate_rank=1, + end_candidate_rank=1, + ): + if start_key == end_key and _distance(start_exit, end_exit) > RoutingNetwork.DEFAULT_NODE_TOLERANCE: return None - path_result = RoutingNetwork.shortest_path_with_carriers( network, start_key, @@ -799,6 +892,7 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non kind_cost_factors=opts.get("carrier_kind_cost_factors", {}), segment_usage_costs=opts.get("segment_usage_costs", {}), segment_reuse_penalty=float(opts.get("segment_reuse_penalty", 0.0) or 0.0), + excluded_transit_carrier_kinds={"TerminalAccess"}, ) path_keys = path_result.get("path", []) if isinstance(path_result, dict) else [] if not path_keys: @@ -813,10 +907,10 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non points = [] for point in start_access_points or [start_origin, start_exit]: _append_unique(points, point) - _append_orthogonal(points, carrier_points[0]) + _append_orthogonal(points, carrier_points[0], obstacle_bboxes=candidate_blocked_bboxes) for point in carrier_points[1:]: _append_unique(points, point) - _append_orthogonal(points, end_exit) + _append_orthogonal(points, end_exit, obstacle_bboxes=candidate_blocked_bboxes) for point in reversed(end_access_points or [end_origin, end_exit]): _append_unique(points, point) points = _simplify_collinear_points( @@ -837,17 +931,128 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "exit_distance": float(end_distance or 0.0), "entry_point_mode": start_mode, "exit_point_mode": end_mode, + "entry_candidate_rank": int(start_candidate_rank or 1), + "exit_candidate_rank": int(end_candidate_rank or 1), "obstacle_aware": bool(obstacle_aware), }, "route_track": path_result, "lane": lane, } + def route_obstacle_hit_count(points): + hits = 0 + if not candidate_blocked_bboxes: + return hits + for index in range(max(len(points or []) - 1, 0)): + start = points[index] + end = points[index + 1] + for bbox in candidate_blocked_bboxes: + if _segment_intersects_bbox(start, end, bbox): + hits += 1 + break + return hits + + def route_on_network(network, obstacle_aware=False): + if network.get("segment_count", 0) <= 0: + return None + + max_distance = float(opts.get("network_entry_max_distance", 0.0) or 0.0) + terminal_access_limit = float(opts.get("terminal_access_max_distance", 0.0) or 0.0) + # 面板只暴露“端子接入最大距离”,最终求路也要遵守它,避免跨很远直接接入孤立网络。 + if terminal_access_limit > 0.0: + max_distance = min(max_distance, terminal_access_limit) if max_distance > 0.0 else terminal_access_limit + candidate_limit = max(int(opts.get("network_entry_candidate_limit", 8) or 0), 1) + start_candidates = RoutingNetwork.rank_connection_point_candidates( + network, + RoutingNetwork.connection_point_candidates( + network, + start_exit, + limit=0, + max_distance=max_distance, + ), + ) + start_candidates = start_candidates[:candidate_limit] + if not start_candidates: + return None + + best_route = None + best_score = None + entry_distance_cost_factor = float( + opts.get("network_entry_distance_cost_factor", 5.0) or 0.0 + ) + for start_rank, start_candidate in enumerate(start_candidates, start=1): + start_network = clone_route_network(network) + start_key, start_distance, start_mode = RoutingNetwork.connect_point_candidate_to_network( + start_network, + start_candidate, + ) + if start_key is None: + continue + end_candidates = RoutingNetwork.rank_connection_point_candidates( + start_network, + RoutingNetwork.connection_point_candidates( + start_network, + end_exit, + limit=0, + max_distance=max_distance, + ), + ) + end_candidates = end_candidates[:candidate_limit] + for end_rank, end_candidate in enumerate(end_candidates, start=1): + working_network = clone_route_network(start_network) + end_key, end_distance, end_mode = RoutingNetwork.connect_point_candidate_to_network( + working_network, + end_candidate, + ) + if end_key is None: + continue + route_data = build_route_payload( + working_network, + start_key, + end_key, + start_distance, + end_distance, + start_mode, + end_mode, + obstacle_aware=obstacle_aware, + start_candidate_rank=start_rank, + end_candidate_rank=end_rank, + ) + if route_data is None: + continue + route_score = float( + (route_data.get("route_track", {}) or {}).get("cost", 0.0) or 0.0 + ) + route_score += ( + float(start_distance or 0.0) + float(end_distance or 0.0) + ) * entry_distance_cost_factor + obstacle_hits = route_obstacle_hit_count(route_data.get("points", [])) + route_score += obstacle_hits * float( + opts.get("route_candidate_collision_penalty", 10000.0) or 0.0 + ) + route_data["network"]["route_candidate_obstacle_hits"] = int(obstacle_hits) + route_data["network"]["entry_candidate_score"] = float(route_score) + if best_score is None or route_score < best_score: + best_score = route_score + best_route = route_data + return best_route + use_obstacle_avoidance = bool(opts.get("avoid_obstacles", True)) obstacles = [] + candidate_obstacles = [] if use_obstacle_avoidance: obstacles = collect_obstacles(doc, exclude=[start_terminal, end_terminal], options=opts) + candidate_options = dict(opts) + candidate_options["ignore_endpoint_near_obstacles"] = False + candidate_obstacles = collect_obstacles( + doc, + exclude=[start_terminal, end_terminal], + options=candidate_options, + ) blocked_bboxes = [obstacle["bbox"] for obstacle in obstacles if obstacle.get("bbox")] + candidate_blocked_bboxes = [ + obstacle["bbox"] for obstacle in candidate_obstacles if obstacle.get("bbox") + ] if blocked_bboxes: obstacle_aware_network = RoutingNetwork.build_route_graph( @@ -986,7 +1191,7 @@ def collect_obstacles(doc, exclude=None, options=None): bbox = _bbox_payload(obj, clearance=clearance) if bbox is None: continue - if endpoint_points and any( + if bool(opts.get("ignore_endpoint_near_obstacles", True)) and endpoint_points and any( _distance_point_to_bbox(point, bbox) <= endpoint_clearance for point in endpoint_points ): @@ -1362,6 +1567,9 @@ def _route_track_segment_keys(route_track): segments = route_track.get("segments", []) if isinstance(route_track, dict) else [] keys = [] for segment in segments or []: + # 虚拟桥接段不是真实线槽/路径共路,不能触发并行 lane 递增或复用惩罚。 + if isinstance(segment, dict) and bool(segment.get("is_bridge", False)): + continue key = _route_segment_key(segment) if key is not None: keys.append(key) @@ -1468,6 +1676,13 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la "skipped_missing_terminal": 0, "skipped_missing_route_network": 0, "skipped_invalid": 0, + "terminal_access_warning_distance": float( + opts.get( + "terminal_access_warning_distance", + RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE, + ) + or 0.0 + ), "missing_endpoint_uuids": [], "missing_endpoint_samples": [], "missing_route_network_samples": [], @@ -1509,6 +1724,35 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la return _set_task_status(_find_task_by_wire_uuid(doc, wire_uuid), status) + def missing_route_network_sample(item, start_uuid, end_uuid, error_text=""): + sample = { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label"), + "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + } + if error_text: + sample["error"] = error_text + return sample + + def add_missing_route_network_sample(item, start_uuid, end_uuid, error_text=""): + if len(report["missing_route_network_samples"]) >= 8: + return + report["missing_route_network_samples"].append( + missing_route_network_sample(item, start_uuid, end_uuid, error_text=error_text) + ) + + def is_missing_route_network_error(error_text): + text = str(error_text or "") + return "没有可用的布线路径网络" in text or "No route path" in text + def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): route_options = dict(options or {}) if isinstance(item, dict) and "__segment_usage_costs" in item: @@ -1567,22 +1811,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la report["skipped_missing_route_network"] += 1 add_status("MissingRouteNetwork") set_item_task_status(item, "MissingRouteNetwork") - if len(report["missing_route_network_samples"]) < 8: - report["missing_route_network_samples"].append( - { - "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), - "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), - "start_terminal_uuid": start_uuid, - "start_element_uuid": _wire_item_value(item, "start_element_uuid"), - "start_terminal_display": _wire_item_value(item, "start_terminal_display"), - "start_device_label": _wire_item_value(item, "start_device_label"), - "end_terminal_uuid": end_uuid, - "end_element_uuid": _wire_item_value(item, "end_element_uuid"), - "end_terminal_display": _wire_item_value(item, "end_terminal_display"), - "end_device_label": _wire_item_value(item, "end_device_label"), - "endpoint_label": _wire_item_value(item, "endpoint_label"), - } - ) + add_missing_route_network_sample(item, start_uuid, end_uuid) continue lane_key = _route_lane_key(start_uuid, end_uuid) route_lane_index = lane_indexes_by_pair.get(lane_key, 0) @@ -1625,6 +1854,13 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la route_segment_keys = _route_segment_keys(result) except Exception as exc: error_text = str(exc) + if is_missing_route_network_error(error_text): + # 路径网络存在但两端无法连通时,按缺路径网络处理,避免被普通 Error 淹没。 + report["skipped_missing_route_network"] += 1 + add_status("MissingRouteNetwork") + set_item_task_status(item, "MissingRouteNetwork") + add_missing_route_network_sample(item, start_uuid, end_uuid, error_text=error_text) + continue report["errors"].append(error_text) add_status("Error") set_item_task_status(item, "Error") @@ -1767,6 +2003,9 @@ def _route_source_labels(route_track, limit=5): if not isinstance(route_track, dict): return labels 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 @@ -1802,6 +2041,14 @@ def _route_network_metric_max(report, key): for route in report.get("routes", []) or []: if not isinstance(route, dict): continue + if key == "bridged_segments": + route_track = route.get("route_track", {}) + if isinstance(route_track, dict) and key in route_track: + try: + maximum = max(maximum, int(route_track.get(key, 0) or 0)) + except Exception: + pass + continue network = route.get("network", {}) if not isinstance(network, dict): continue @@ -1815,6 +2062,7 @@ def _route_network_metric_max(report, key): def _route_lane_summary(report): max_lane_index = 0 lane_spacing = 0.0 + lane_max_offset = 0.0 for route in report.get("routes", []) or []: if not isinstance(route, dict): continue @@ -1832,11 +2080,16 @@ def _route_lane_summary(report): lane_spacing = float(lane.get("spacing_mm", 0.0) or 0.0) except Exception: lane_spacing = 0.0 + try: + lane_max_offset = float(lane.get("max_offset_mm", 0.0) or 0.0) + except Exception: + lane_max_offset = 0.0 if max_lane_index <= 0: return {} return { "max_lane_index": max_lane_index, "spacing_mm": lane_spacing, + "max_offset_mm": lane_max_offset, } @@ -1845,6 +2098,9 @@ def _route_track_min_capacity(route_track): return None capacities = [] 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 @@ -1891,6 +2147,24 @@ _ROUTE_QUALITY_WARNING_KIND_LABELS = { def _route_track_carrier_kinds(route_track): if not isinstance(route_track, dict): return {} + counts = {} + has_segment_list = isinstance(route_track.get("segments"), list) + raw_segments = route_track.get("segments", []) + segments = raw_segments or [] + for segment in segments: + # 虚拟桥接段只表示网络连通,不代表导线真实经过该类型 carrier。 + 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 + kind = str(carrier.get("kind", "") or "").strip() + if kind: + counts[kind] = counts.get(kind, 0) + 1 + if counts: + return counts + if has_segment_list: + return {} carrier_kinds = route_track.get("carrier_kinds", {}) if isinstance(carrier_kinds, dict) and carrier_kinds: return { @@ -1898,15 +2172,32 @@ def _route_track_carrier_kinds(route_track): for key, value in carrier_kinds.items() if str(key).strip() } - counts = {} + return {} + + +def _route_track_carrier_names(route_track, limit=8): + if not isinstance(route_track, dict): + return [] + names = [] + seen = set() + has_segment_list = isinstance(route_track.get("segments"), list) for segment in route_track.get("segments", []) or []: + # 诊断样例只列真实经过的 carrier;虚拟桥接段不显示为源路径对象。 + 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 - kind = str(carrier.get("kind", "") or "").strip() - if kind: - counts[kind] = counts.get(kind, 0) + 1 - return counts + name = str(carrier.get("name", "") or "").strip() + if not name or name in seen: + continue + seen.add(name) + names.append(name) + if len(names) >= int(limit or 0): + return names + if has_segment_list: + return names + return list(route_track.get("carrier_names", []) or [])[: int(limit or 0)] def _route_quality_warning_summary(report): @@ -1961,6 +2252,72 @@ def _route_quality_warning_samples(report, limit=8): return samples +def _long_network_entry_warning_samples(report, limit=8): + try: + warning_distance = float( + report.get( + "terminal_access_warning_distance", + RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE, + ) + or 0.0 + ) + except Exception: + warning_distance = float(RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE) + if warning_distance <= 0.0: + return [] + + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + long_parts = [] + warning_sides = [] + for side, label, key in ( + ("entry", "起点", "entry_distance"), + ("exit", "终点", "exit_distance"), + ): + try: + distance = float(network.get(key, 0.0) or 0.0) + except Exception: + distance = 0.0 + if distance > warning_distance: + warning_sides.append(side) + long_parts.append("{0}接入 {1:.1f} mm".format(label, distance)) + if not long_parts: + continue + if max_samples <= 0 or len(samples) < max_samples: + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire": _wire_sample_text(route), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "entry_distance": float(network.get("entry_distance", 0.0) or 0.0), + "exit_distance": float(network.get("exit_distance", 0.0) or 0.0), + "warning_sides": warning_sides, + "warning_parts": long_parts, + "warning_distance": float(warning_distance), + } + ) + return samples + + +def _long_network_entry_summary(report): + samples = _long_network_entry_warning_samples(report, limit=1) + if not samples: + return {} + return { + "count": len(_long_network_entry_warning_samples(report, limit=0)), + "sample": samples[0], + "warning_distance": float(samples[0].get("warning_distance", 0.0) or 0.0), + } + + def format_eplan_connection_route_report(report): message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( report.get("routed", 0), @@ -2019,6 +2376,16 @@ def format_eplan_connection_route_report(report): message += "\n缺少布线路径网络:{0} 条导线已跳过。请先生成线槽、布线面或布线路径网络。".format( report.get("skipped_missing_route_network", 0) ) + route_sample = (report.get("missing_route_network_samples") or [None])[0] + if route_sample: + message += "\n缺路径网络示例:导线 {0},{1}。".format( + _wire_sample_text(route_sample), + _endpoint_pair_text(route_sample), + ) + # 示例带上原始失败原因,手动测试时可以直接判断是空网络、端点不连通还是距离阈值限制。 + route_error = str(route_sample.get("error", "") or "").strip() + if route_error: + message += "原因:{0}。".format(route_error) total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) if total_length_mm > 0.0: message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) @@ -2032,23 +2399,61 @@ def format_eplan_connection_route_report(report): blocked_segments = _route_network_metric_max(report, "blocked_segments") network_parts = [] if bridged_segments > 0: - network_parts.append("自动桥接 {0} 段相邻主路径".format(bridged_segments)) + network_parts.append("自动桥接 {0} 段相邻/投影主路径".format(bridged_segments)) if blocked_segments > 0: network_parts.append("避障屏蔽 {0} 段".format(blocked_segments)) if network_parts: message += "\n路径网络:{0}。".format(",".join(network_parts)) lane_summary = _route_lane_summary(report) if lane_summary: - message += "\n并行错位:最大 lane {0},间距 {1:.1f} mm。".format( + lane_text = "并行错位:最大 lane {0},间距 {1:.1f} mm".format( lane_summary.get("max_lane_index", 0), float(lane_summary.get("spacing_mm", 0.0) or 0.0), ) + max_offset = float(lane_summary.get("max_offset_mm", 0.0) or 0.0) + if max_offset > 0.0: + lane_text += ",最大偏移 {0:.1f} mm".format(max_offset) + message += "\n{0}。".format(lane_text) capacity_pressure = _route_capacity_pressure_summary(report) if capacity_pressure: message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}。".format( capacity_pressure.get("max_parallel_wires", 0), capacity_pressure.get("min_capacity", 0), ) + candidate_ranks = [] + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + try: + entry_rank = int(network.get("entry_candidate_rank", 0) or 0) + except Exception: + entry_rank = 0 + try: + exit_rank = int(network.get("exit_candidate_rank", 0) or 0) + except Exception: + exit_rank = 0 + if entry_rank > 1 or exit_rank > 1: + candidate_ranks.append((entry_rank, exit_rank)) + if candidate_ranks: + entry_rank, exit_rank = candidate_ranks[0] + parts = [] + if entry_rank > 1: + parts.append("起点第 {0} 个".format(entry_rank)) + if exit_rank > 1: + parts.append("终点第 {0} 个".format(exit_rank)) + message += "\n接入候选:{0}。".format(",".join(parts)) + long_entry_warning = _long_network_entry_summary(report) + if long_entry_warning: + sample = long_entry_warning.get("sample", {}) + # 最终导线接入距离过长时,通常意味着设备附近缺少局部路径或主路径离端子太远。 + message += "\n接入距离提示:{0} 条导线起点/终点接入过长,示例导线 {1} {2},可能存在悬空或跨距过长。".format( + long_entry_warning.get("count", 0), + sample.get("wire", "未知导线"), + ",".join(sample.get("warning_parts", []) or []), + ) route_source_sample = _route_source_sample_text(report) if route_source_sample: message += "\n{0}".format(route_source_sample) @@ -2167,16 +2572,19 @@ def _compact_route_sample(route): "lane": route.get("lane", {}), "algorithm": route.get("algorithm", ""), "collision_count": route.get("collision_count", 0), - "carrier_kinds": route_track.get("carrier_kinds", {}), - "carrier_names": list(route_track.get("carrier_names", []) or [])[:8], + "carrier_kinds": _route_track_carrier_kinds(route_track), + "carrier_names": _route_track_carrier_names(route_track, limit=8), "route_source_labels": _route_source_labels(route_track, limit=8), } network = route.get("network", {}) if isinstance(network, dict): + bridged_segments = network.get("bridged_segments", 0) + if "bridged_segments" in route_track: + bridged_segments = route_track.get("bridged_segments", 0) sample["network"] = { "carriers": network.get("carriers", 0), "segments": network.get("segments", 0), - "bridged_segments": network.get("bridged_segments", 0), + "bridged_segments": bridged_segments, "blocked_segments": network.get("blocked_segments", 0), "entry_distance": network.get("entry_distance", 0.0), "exit_distance": network.get("exit_distance", 0.0), @@ -2236,6 +2644,11 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): 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 + entry_distance_warnings = _long_network_entry_warning_samples(report, limit=limit) + payload["route_entry_distance_warning_count"] = len( + _long_network_entry_warning_samples(report, limit=0) + ) + payload["route_entry_distance_warning_samples"] = entry_distance_warnings payload["diagnostic_payload"] = "compact-routing-connection-batch-v1" return payload @@ -2446,12 +2859,14 @@ def _format_point_text(point): def _diagnostic_terminal_text(item): if not isinstance(item, dict): return "未知端子" - return ( - item.get("terminal_uuid") - or item.get("label") - or item.get("name") - or "未知端子" - ) + terminal_uuid = str(item.get("terminal_uuid", "") or "").strip() + label = str(item.get("label", "") or "").strip() + name = str(item.get("name", "") or "").strip() + # 报告优先给人看的 FreeCAD 对象名,同时保留 terminal_uuid 便于和 QET 数据对应。 + display = label if label and label != terminal_uuid else name + if display and terminal_uuid and display != terminal_uuid: + return "{0}({1})".format(display, terminal_uuid) + return terminal_uuid or display or "未知端子" def _dict_items(value): @@ -2475,7 +2890,7 @@ def format_routing_path_network_report(diagnostic): ) bridged_segments = int(summary.get("bridged_segments", 0) or 0) if bridged_segments > 0: - message += " 自动桥接 {0} 段相邻主路径。".format(bridged_segments) + message += " 自动桥接 {0} 段相邻/投影主路径。".format(bridged_segments) return message message = "布线路径网络检查发现 {0} 类问题。".format(len(issues)) diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index f3ff62e..3ce310b 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -132,6 +132,13 @@ class AutoRoutingController: lane_spacing = AutoRouting.DEFAULT_OPTIONS["lane_spacing"] self.options["lane_spacing"] = max(lane_spacing, 0.0) + def set_lane_max_offset(self, value): + try: + lane_max_offset = float(value) + except Exception: + lane_max_offset = AutoRouting.DEFAULT_OPTIONS["lane_max_offset"] + self.options["lane_max_offset"] = max(lane_max_offset, 0.0) + def set_lane_axis(self, value): lane_axis = str(value or "").strip().lower() if lane_axis not in {"auto", "x", "y", "z"}: @@ -294,6 +301,9 @@ class AutoRoutingTaskPanel: self.adjoining_duct_tolerance_spin.setRange(0.0, 1000.0) self.adjoining_duct_tolerance_spin.setDecimals(1) self.adjoining_duct_tolerance_spin.setSingleStep(1.0) + self.adjoining_duct_tolerance_spin.setToolTip( + "用于相邻主路径端点桥接,也用于支路端点到主路径中段的投影桥接。" + ) self.adjoining_duct_tolerance_spin.setValue( float( self.controller.routing_options().get( @@ -345,6 +355,20 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.lane_spacing_spin) + options_layout.addWidget(QtWidgets.QLabel("并行线最大偏移 mm")) + self.lane_max_offset_spin = QtWidgets.QDoubleSpinBox() + self.lane_max_offset_spin.setRange(0.0, 1000.0) + self.lane_max_offset_spin.setDecimals(1) + self.lane_max_offset_spin.setSingleStep(1.0) + self.lane_max_offset_spin.setValue( + float( + self.controller.routing_options().get( + "lane_max_offset", + AutoRouting.DEFAULT_OPTIONS["lane_max_offset"], + ) + ) + ) + options_layout.addWidget(self.lane_max_offset_spin) options_layout.addWidget(QtWidgets.QLabel("并行线方向")) self.lane_axis_combo = QtWidgets.QComboBox() self.lane_axis_combo.addItems(["auto", "x", "y", "z"]) @@ -441,6 +465,7 @@ class AutoRoutingTaskPanel: self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value()) self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value()) self.controller.set_lane_spacing(self.lane_spacing_spin.value()) + self.controller.set_lane_max_offset(self.lane_max_offset_spin.value()) self.controller.set_lane_axis(self.lane_axis_combo.currentText()) def generate_routing_paths(self): diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 21b33ed..1157dec 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -48,6 +48,9 @@ DEFAULT_ROUTE_PATH_FACE_OFFSET = 2.0 DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT = 2.5 DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE = 1000.0 DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE = 500.0 +DEFAULT_TERMINAL_ACCESS_COMPONENT_SEGMENT_PENALTY = 25.0 +DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY = 1000.0 +DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY = 2000.0 DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0 DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 WIRE_DUCT_OBSTACLE_MODE = "PassThrough" @@ -125,7 +128,7 @@ DEFAULT_KIND_COST_FACTORS = { ROUTE_CARRIER_KIND: 1.0, ROUTE_CARRIER_KIND_AUXILIARY_PATH: 2.0, ROUTE_CARRIER_KIND_TERMINAL_ACCESS: 2.0, - ROUTE_CARRIER_KIND_ROUTING_RANGE: 25.0, + ROUTE_CARRIER_KIND_ROUTING_RANGE: 40.0, ROUTE_CARRIER_KIND_USER_PATH: 1.0, } ROUTE_CARRIER_VIEW_STYLES = { @@ -2402,6 +2405,106 @@ def _orthogonal_access_points(start, end): return points +def _is_primary_route_carrier(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, + ROUTE_CARRIER_KIND_AUXILIARY_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 {} + seen = set() + metrics_by_node = {} + for start_key in nodes.keys(): + if start_key in seen: + continue + stack = [start_key] + seen.add(start_key) + component_nodes = [] + component_edges = set() + primary_edges = set() + while stack: + key = stack.pop() + component_nodes.append(key) + for next_key, _weight, carrier in edges.get(key, []) or []: + edge_key = tuple(sorted((key, next_key))) + component_edges.add(edge_key) + if _is_primary_route_carrier(carrier): + primary_edges.add(edge_key) + if next_key not in seen: + seen.add(next_key) + stack.append(next_key) + metrics = { + "segments": len(component_edges), + "primary_segments": len(primary_edges), + } + for key in component_nodes: + metrics_by_node[key] = metrics + return metrics_by_node + + +def rank_connection_point_candidates(network, candidates): + """Sort graph entry candidates by route usefulness, not only distance.""" + candidates = [candidate for candidate in list(candidates or []) if isinstance(candidate, dict)] + if not candidates: + return [] + metrics_by_node = _component_metrics_by_node(network) + max_segments = max( + [int(metrics.get("segments", 0) or 0) for metrics in metrics_by_node.values()] or [0] + ) + max_primary_segments = max( + [int(metrics.get("primary_segments", 0) or 0) for metrics in metrics_by_node.values()] + or [0] + ) + ranked = [] + for candidate in candidates: + left_metrics = metrics_by_node.get(candidate.get("key"), {}) + right_metrics = metrics_by_node.get(candidate.get("next_key"), {}) + component_segments = max( + int(left_metrics.get("segments", 0) or 0), + int(right_metrics.get("segments", 0) or 0), + ) + component_primary_segments = max( + int(left_metrics.get("primary_segments", 0) or 0), + int(right_metrics.get("primary_segments", 0) or 0), + ) + score = float(candidate.get("distance", 0.0) or 0.0) + if max_primary_segments > 0 and component_primary_segments <= 0: + score += DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY + carrier_kind = (getattr(candidate.get("carrier"), "QetRouteCarrierKind", "") or "").strip() + if max_primary_segments > 0 and carrier_kind == ROUTE_CARRIER_KIND_TERMINAL_ACCESS: + # 入口候选也要优先真实主路径,避免导线贴到其它端子的局部接入线上起步。 + score += DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY + score += max(0, max_segments - component_segments) * DEFAULT_TERMINAL_ACCESS_COMPONENT_SEGMENT_PENALTY + item = dict(candidate) + item["route_entry_score"] = float(score) + item["route_entry_component_segments"] = int(component_segments) + item["route_entry_component_primary_segments"] = int(component_primary_segments) + ranked.append(item) + ranked.sort(key=lambda item: float(item.get("route_entry_score", 0.0) or 0.0)) + return ranked + + +def _terminal_access_target_candidate(network, exit_point, max_distance): + candidates = connection_point_candidates( + network, + exit_point, + limit=0, + max_distance=max_distance, + ) + ranked = rank_connection_point_candidates(network, candidates) + if not ranked: + return None + return ranked[0] + + def create_terminal_access_carriers_from_document( doc, project_uuid="", @@ -2444,9 +2547,11 @@ def create_terminal_access_carriers_from_document( if len(terminal_access_points) < 2: continue exit_point = terminal_access_points[-1] - nearest_point, distance = nearest_point_on_network(network, exit_point) - if nearest_point is None: + candidate = _terminal_access_target_candidate(network, exit_point, max_distance) + if candidate is None: continue + nearest_point = _vector(candidate.get("point")) + distance = float(candidate.get("distance", 0.0) or 0.0) if max_distance and float(distance or 0.0) > float(max_distance): continue if float(distance or 0.0) <= DEFAULT_NODE_TOLERANCE: @@ -2716,6 +2821,8 @@ def build_route_graph( blocked_bboxes = list(blocked_bboxes or []) segments = [] bridgeable_endpoint_nodes = [] + projection_bridge_candidates = [] + adjoining_limit = max(float(adjoining_duct_tolerance or 0.0), 0.0) def ensure_node(point): key = _point_key(point, tolerance=tolerance) @@ -2760,6 +2867,31 @@ def build_route_graph( left["points"].extend(intersections) right["points"].extend(intersections) + # 现场线槽/UserPath 常见“支路端点靠近主干中段”,并不总是端点对端点。 + # 在容差内时先把主干投影点加入分段点,后面再补一条虚拟桥接边。 + if adjoining_limit > tolerance: + for left in segments: + left_carrier = left["carrier"] + left_kind = (getattr(left_carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + if left_kind not in BRIDGEABLE_ENDPOINT_CARRIER_KINDS: + continue + for endpoint in (left["start"], left["end"]): + for right in segments: + right_carrier = right["carrier"] + if right_carrier is left_carrier: + continue + right_kind = (getattr(right_carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + if right_kind not in BRIDGEABLE_ENDPOINT_CARRIER_KINDS: + continue + projected = _closest_point_on_segment(endpoint, right["start"], right["end"]) + distance = _distance(endpoint, projected) + if distance <= tolerance or distance > adjoining_limit: + continue + right["points"].append(projected) + projection_bridge_candidates.append( + (endpoint, projected, left_carrier, right_carrier) + ) + for segment in segments: ordered = _sorted_segment_points( segment["start"], @@ -2793,35 +2925,52 @@ def build_route_graph( previous_key = current_key previous_point = current_point - adjoining_limit = max(float(adjoining_duct_tolerance or 0.0), 0.0) bridged_pairs = set() + + def add_bridge_edge(left_key, left_point, left_carrier, right_key, right_point, right_carrier): + nonlocal blocked_segment_count, bridged_segment_count, segment_count + if left_key == right_key or left_carrier is right_carrier: + return + pair = tuple(sorted((left_key, right_key))) + if pair in bridged_pairs: + return + distance = _distance(left_point, right_point) + if distance <= tolerance or distance > adjoining_limit: + return + if any(next_key == right_key for next_key, _weight, _carrier in edges.get(left_key, [])): + return + if _segment_hits_blocked_bbox(left_point, right_point, blocked_bboxes): + blocked_segment_count += 1 + return + edges[left_key].append((right_key, distance, left_carrier)) + edges[right_key].append((left_key, distance, right_carrier)) + segment_count += 1 + bridged_segment_count += 1 + bridged_pairs.add(pair) + if adjoining_limit > tolerance: + for endpoint, projected, endpoint_carrier, projected_carrier in projection_bridge_candidates: + endpoint_key = ensure_node(endpoint) + projected_key = ensure_node(projected) + add_bridge_edge( + endpoint_key, + nodes[endpoint_key], + endpoint_carrier, + projected_key, + nodes[projected_key], + projected_carrier, + ) for left_index, left in enumerate(bridgeable_endpoint_nodes): left_key, left_point, left_carrier = left for right_key, right_point, right_carrier in bridgeable_endpoint_nodes[left_index + 1:]: - if left_key == right_key or left_carrier is right_carrier: - continue - pair = tuple(sorted((left_key, right_key))) - if pair in bridged_pairs: - continue - distance = _distance(left_point, right_point) - if distance <= tolerance or distance > adjoining_limit: - continue - if any(next_key == right_key for next_key, _weight, _carrier in edges.get(left_key, [])): - continue - if _segment_hits_blocked_bbox(left_point, right_point, blocked_bboxes): - blocked_segment_count += 1 - continue - edges[left_key].append((right_key, distance, left_carrier)) - edges[right_key].append((left_key, distance, right_carrier)) - segment_count += 1 - bridged_segment_count += 1 - bridged_pairs.add(pair) + add_bridge_edge(left_key, left_point, left_carrier, right_key, right_point, right_carrier) return { "nodes": nodes, "edges": edges, "carriers": carriers, + # 自动桥接边只存在于路径图里;保存 key 对用于 route track 标记实际走过的桥接段。 + "bridge_pairs": set(bridged_pairs), "carrier_count": len(carriers), "segment_count": segment_count, "bridged_segment_count": bridged_segment_count, @@ -2885,18 +3034,19 @@ def nearest_point_on_network(network, point): return nearest_node(network, target) -def connect_point_to_network(network, point): - """Connect the closest projected point to a route graph and return key/distance/mode.""" +def connection_point_candidates(network, point, limit=8, max_distance=0.0): + """Return nearby graph entry candidates sorted by distance.""" if not isinstance(network, dict): - return None, None, "none" + return [] nodes = network.get("nodes", {}) or {} edges = network.get("edges", {}) or {} if not nodes or not edges: - return None, None, "none" + return [] tolerance = float(network.get("tolerance", DEFAULT_NODE_TOLERANCE) or DEFAULT_NODE_TOLERANCE) target = _vector(point) - best = None + candidates = [] + seen_candidates = set() seen = set() for key, neighbors in edges.items(): start = nodes.get(key) @@ -2912,28 +3062,71 @@ def connect_point_to_network(network, point): continue projected = _closest_point_on_segment(target, start, end) distance = _distance(target, projected) - if best is None or distance < best["distance"]: - best = { + if max_distance > 0.0 and distance > max_distance: + continue + projected_key = _point_key(projected, tolerance=tolerance) + candidate_key = (projected_key, key, next_key, id(carrier)) + if candidate_key in seen_candidates: + continue + seen_candidates.add(candidate_key) + candidates.append( + { "key": key, "next_key": next_key, "carrier": carrier, "point": projected, + "projected_key": projected_key, "distance": distance, } + ) - if best is None: + if not candidates: node_key, distance = nearest_node(network, target) - return node_key, distance, "node" if node_key is not None else "none" + if node_key is None: + return [] + if max_distance > 0.0 and float(distance or 0.0) > max_distance: + return [] + candidates.append( + { + "key": node_key, + "next_key": None, + "carrier": None, + "point": nodes.get(node_key, target), + "projected_key": node_key, + "distance": float(distance or 0.0), + "mode": "node", + } + ) - projected_key = _point_key(best["point"], tolerance=tolerance) + candidates.sort(key=lambda item: float(item.get("distance", 0.0) or 0.0)) + max_items = max(int(limit or 0), 0) + if max_items: + return candidates[:max_items] + return candidates + + +def connect_point_candidate_to_network(network, candidate): + """Connect a preselected projected point to a route graph.""" + if not isinstance(network, dict) or not isinstance(candidate, dict): + return None, None, "none" + nodes = network.get("nodes", {}) or {} + edges = network.get("edges", {}) or {} + if not nodes or not edges: + return None, None, "none" + + tolerance = float(network.get("tolerance", DEFAULT_NODE_TOLERANCE) or DEFAULT_NODE_TOLERANCE) + projected = _vector(candidate.get("point")) + projected_key = candidate.get("projected_key") or _point_key(projected, tolerance=tolerance) if projected_key in nodes: - return projected_key, best["distance"], "node" + return projected_key, float(candidate.get("distance", 0.0) or 0.0), "node" - start_key = best["key"] - end_key = best["next_key"] + start_key = candidate.get("key") + end_key = candidate.get("next_key") + if start_key not in nodes or end_key not in nodes: + return None, None, "none" start = nodes[start_key] end = nodes[end_key] - carrier = best["carrier"] + carrier = candidate.get("carrier") def remove_edge_once(left_key, right_key, fallback_to_pair=False): neighbors = list(edges.get(left_key, []) or []) @@ -2953,12 +3146,12 @@ def connect_point_to_network(network, point): removed_forward = remove_edge_once(start_key, end_key) remove_edge_once(end_key, start_key, fallback_to_pair=removed_forward) - nodes[projected_key] = best["point"] + nodes[projected_key] = projected edges[projected_key] = [] added_segments = 0 for left_key, left_point, right_key, right_point in ( - (start_key, start, projected_key, best["point"]), - (projected_key, best["point"], end_key, end), + (start_key, start, projected_key, projected), + (projected_key, projected, end_key, end), ): weight = _distance(left_point, right_point) if weight <= tolerance: @@ -2967,7 +3160,15 @@ def connect_point_to_network(network, point): edges[right_key].append((left_key, weight, carrier)) added_segments += 1 network["segment_count"] = max(int(network.get("segment_count", 0) or 0) - 1 + added_segments, 0) - return projected_key, best["distance"], "segment_projection" + return projected_key, float(candidate.get("distance", 0.0) or 0.0), "segment_projection" + + +def connect_point_to_network(network, point): + """Connect the closest projected point to a route graph and return key/distance/mode.""" + candidates = connection_point_candidates(network, point, limit=1) + if not candidates: + return None, None, "none" + return connect_point_candidate_to_network(network, candidates[0]) def _carrier_track_payload(carrier): @@ -3018,6 +3219,7 @@ def shortest_path_with_carriers( kind_cost_factors=None, segment_usage_costs=None, segment_reuse_penalty=0.0, + excluded_transit_carrier_kinds=None, ): """Dijkstra search with a small extra cost when route direction changes.""" if start_key is None or end_key is None: @@ -3026,11 +3228,18 @@ def shortest_path_with_carriers( return { "path": [start_key], "segments": [], + "bridged_segments": 0, "cost": 0.0, } nodes = network.get("nodes", {}) edges = network.get("edges", {}) + bridge_pairs = set(network.get("bridge_pairs", set()) or set()) + excluded_transit_kinds = { + str(kind or "").strip() + for kind in (excluded_transit_carrier_kinds or []) + if str(kind or "").strip() + } queue = [] counter = 0 start_state = (start_key, None) @@ -3053,16 +3262,18 @@ def shortest_path_with_carriers( previous_key = previous_state[0] current_key = current_state[0] carrier = previous_entry.get("carrier") - segments.append( - { - "from_key": list(previous_key), - "to_key": list(current_key), - "from": _point_payload(nodes[previous_key]), - "to": _point_payload(nodes[current_key]), - "length_mm": float(previous_entry.get("weight", 0.0) or 0.0), - "carrier": _carrier_track_payload(carrier), - } - ) + segment_pair = tuple(sorted((previous_key, current_key))) + segment_payload = { + "from_key": list(previous_key), + "to_key": list(current_key), + "from": _point_payload(nodes[previous_key]), + "to": _point_payload(nodes[current_key]), + "length_mm": float(previous_entry.get("weight", 0.0) or 0.0), + "carrier": _carrier_track_payload(carrier), + } + if segment_pair in bridge_pairs: + segment_payload["is_bridge"] = True + segments.append(segment_payload) current_state = previous_state path.append(current_state[0]) path.reverse() @@ -3070,7 +3281,12 @@ def shortest_path_with_carriers( carrier_names = [] carrier_kinds = {} + bridged_segments = 0 for segment in segments: + if bool(segment.get("is_bridge", False)): + bridged_segments += 1 + # 桥接段是虚拟连通边,不纳入“真实经过的 carrier 类型”汇总。 + continue carrier = segment.get("carrier", {}) name = carrier.get("name", "") if name and name not in carrier_names: @@ -3083,10 +3299,15 @@ def shortest_path_with_carriers( "segments": segments, "carrier_names": carrier_names, "carrier_kinds": carrier_kinds, + "bridged_segments": bridged_segments, "cost": float(cost), } for next_key, weight, carrier in edges.get(key, []): + carrier_kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + # TerminalAccess 是端子局部接入线,不能被其它导线当作柜内主路径或公共桥接段。 + if carrier_kind in excluded_transit_kinds: + continue direction = _direction_key(nodes[key], nodes[next_key]) bend_cost = 0.0 if previous_direction is not None and direction != previous_direction: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 719f249..8372a37 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -843,6 +843,146 @@ class AutoRoutingTest(unittest.TestCase): self.assertIsNotNone(result) self.assertIn("UserPath", result["carrier_kinds"]) + def test_route_graph_bridges_endpoint_to_nearby_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") + 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(50, 8, 20), app.Vector(50, 50, 20)], + project_uuid="project-1", + kind="UserPath", + ) + + network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) + start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) + end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) + result = routing_network.shortest_path_with_carriers(network, start_key, end_key) + + self.assertEqual(1, network["bridged_segment_count"]) + self.assertIsNotNone(result) + self.assertIn((50000, 0, 20000), result["path"]) + + def test_auto_routing_uses_endpoint_to_segment_projection_bridge(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalBranch", "terminal-branch", app.Vector(50, 50, 0)) + end = _terminal(doc, terminal_objects, "TerminalMain", "terminal-main", 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", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], + project_uuid="project-1", + kind="UserPath", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"adjoining_duct_tolerance": 15.0}, + ) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(1, result["network"]["bridged_segments"]) + self.assertEqual(1, result["route_track"]["bridged_segments"]) + self.assertTrue(any(segment.get("is_bridge") for segment in result["route_track"]["segments"])) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + + def test_auto_routing_does_not_use_terminal_access_to_bridge_main_path_gap(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(40, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(40, 0, 20), app.Vector(60, 0, 20)], + project_uuid="project-1", + kind="TerminalAccess", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_access_max_distance": 5.0}, + ) + + def test_route_graph_projection_bridge_respects_blocked_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") + 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(50, 8, 20), app.Vector(50, 50, 20)], + project_uuid="project-1", + kind="UserPath", + ) + blocked_bboxes = [ + { + "xmin": 45.0, + "xmax": 55.0, + "ymin": 2.0, + "ymax": 6.0, + "zmin": 15.0, + "zmax": 25.0, + } + ] + + network = routing_network.build_route_graph( + doc, + blocked_bboxes=blocked_bboxes, + adjoining_duct_tolerance=15.0, + ) + start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) + end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) + result = routing_network.shortest_path_with_carriers(network, start_key, end_key) + + self.assertEqual(0, network["bridged_segment_count"]) + self.assertGreaterEqual(network["blocked_segment_count"], 1) + self.assertIsNone(result) + def test_auto_routing_respects_adjoining_duct_tolerance_option(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -1644,6 +1784,97 @@ class AutoRoutingTest(unittest.TestCase): 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(0, 1, 20), app.Vector(5, 1, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + 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", + kind="WireDuct", + ) + + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + ) + + 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), + ) + + first_kind = getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "") + self.assertEqual("WireDuct", first_kind) + + 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") + _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", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 10, 20), app.Vector(120, 10, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + ) + + 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_eplan_connection_route_enters_network_at_segment_projection(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -1840,6 +2071,8 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("terminal-long-access", payload["long_terminal_accesses"][0]["terminal_uuid"]) self.assertEqual(900.0, payload["long_terminal_accesses"][0]["terminal_access_length_mm"]) self.assertIn("端子接入过长", message) + self.assertIn("TerminalLongAccess", message) + self.assertIn("terminal-long-access", message) self.assertIn("900.0 mm", message) def test_check_routing_path_network_warns_for_invalid_terminal_local_route_points(self): @@ -2042,7 +2275,7 @@ class AutoRoutingTest(unittest.TestCase): message = auto_routing.format_routing_path_network_report(diagnostic) - self.assertIn("桥接 1 段相邻主路径", message) + self.assertIn("桥接 1 段相邻/投影主路径", message) def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self): _install_fake_freecad() @@ -2194,7 +2427,7 @@ class AutoRoutingTest(unittest.TestCase): 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)) + _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)], @@ -2337,6 +2570,39 @@ class AutoRoutingTest(unittest.TestCase): 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() + 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)], + 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( + doc, + start, + end, + route_index=21, + options=controller.routing_options(), + ) + + 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_auto_routing_panel_command_button_style_keeps_text_visible(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() @@ -2638,6 +2904,77 @@ class AutoRoutingTest(unittest.TestCase): self.assertGreaterEqual(result["network"]["blocked_segments"], 1) self.assertIn(50.0, [point.y for point in result["points"]]) + 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(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}, + ) + + 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"]) + + 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(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}, + ) + + 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_route_marks_collision_warning_against_obstacle_bbox(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -2771,7 +3108,7 @@ class AutoRoutingTest(unittest.TestCase): 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)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) payload = { "project_uuid": "project-1", "wires": [ @@ -2857,9 +3194,67 @@ class AutoRoutingTest(unittest.TestCase): 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): + 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() + 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") @@ -2898,7 +3293,11 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + 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"]) @@ -2909,6 +3308,79 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual([], report["errors"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + 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([], wiring_objects.iter_routed_wire_objects(doc)) + + 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() @@ -2988,6 +3460,48 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("wire-a", diagnostic_payload["route_samples"][0]["wire_uuid"]) self.assertEqual("Routed", diagnostic_payload["route_samples"][0]["route_status"]) + 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_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({"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() @@ -3029,6 +3543,38 @@ class AutoRoutingTest(unittest.TestCase): diagnostic_payload["route_quality_warning_samples"][0]["carrier_kinds"], ) + def test_compact_batch_report_includes_entry_distance_warning_samples(self): + _install_fake_freecad() + _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_uuid": "wire-long-entry", + "wire_label": "N-LONG", + "network": { + "entry_distance": 125.0, + "exit_distance": 20.0, + }, + } + ], + } + + 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( + ["entry"], + payload["route_entry_distance_warning_samples"][0]["warning_sides"], + ) + def test_route_eplan_connections_reports_total_connection_route_length(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -3245,6 +3791,83 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message) + def test_route_report_source_sample_skips_bridge_segments(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_label": "N4111", + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + {"is_bridge": True, "carrier": {"kind": "WireDuct", "source_label": "虚拟桥接"}}, + {"carrier": {"kind": "UserPath", "source_label": "用户路径B"}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 线槽A、用户路径B。", message) + self.assertNotIn("虚拟桥接", message) + + def test_route_track_segment_keys_skip_bridge_segments(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + route_track = { + "segments": [ + { + "from_key": [0, 0, 0], + "to_key": [100, 0, 0], + "carrier": {"name": "WireDuctA"}, + }, + { + "is_bridge": True, + "from_key": [100, 0, 0], + "to_key": [100, 10, 0], + "carrier": {"name": "VirtualBridge"}, + }, + ] + } + + keys = auto_routing._route_track_segment_keys(route_track) + + self.assertEqual(1, len(keys)) + self.assertEqual("WireDuctA", keys[0][0]) + + def test_route_quality_warning_ignores_bridge_only_routing_range(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_label": "N4111", + "route_track": { + "carrier_kinds": {"RoutingRange": 1}, + "segments": [ + { + "is_bridge": True, + "carrier": {"kind": "RoutingRange", "source_label": "虚拟布线面桥接"}, + } + ], + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertNotIn("路径质量提示", message) + def test_route_report_includes_network_bridge_and_blocked_segment_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -3264,7 +3887,31 @@ class AutoRoutingTest(unittest.TestCase): message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径网络:自动桥接 1 段相邻主路径,避障屏蔽 2 段。", message) + self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径,避障屏蔽 2 段。", message) + + def test_route_report_prefers_route_track_bridged_segment_count(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": [ + { + "network": { + "bridged_segments": 3, + }, + "route_track": { + "bridged_segments": 1, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径。", message) + self.assertNotIn("自动桥接 3 段", message) def test_route_report_includes_parallel_lane_summary(self): _install_fake_freecad() @@ -3274,14 +3921,43 @@ class AutoRoutingTest(unittest.TestCase): "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ - {"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "offset_mm": 0.0}}, - {"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "offset_mm": -10.0}}, + {"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": 0.0}}, + {"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": -10.0}}, ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("并行错位:最大 lane 2,间距 10.0 mm。", message) + self.assertIn("并行错位:最大 lane 2,间距 10.0 mm,最大偏移 30.0 mm。", message) + + def test_eplan_connection_lane_offset_is_capped_for_dense_parallel_routes(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + route_index=21, + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + + self.assertEqual(30.0, result["lane"]["offset_mm"]) + self.assertLessEqual( + max(abs(point.y) for point in result["points"]), + 30.0, + ) def test_route_report_includes_replaced_routed_connection_count(self): _install_fake_freecad() @@ -3370,6 +4046,86 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("容量提示:最大并行线数 3,路径最小容量 2。", message) + def test_route_report_ignores_bridge_segments_for_capacity_pressure(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": [ + { + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"is_bridge": True, "carrier": {"kind": "UserPath", "capacity": 1}}, + {"carrier": {"kind": "WireDuct", "capacity": 4}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertNotIn("容量提示", message) + + def test_route_report_includes_entry_candidate_rank_when_route_uses_fallback_entry(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_label": "N1", + "network": { + "entry_candidate_rank": 3, + "exit_candidate_rank": 1, + "entry_candidate_score": 125.0, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("接入候选", message) + self.assertIn("起点第 3 个", message) + + def test_route_report_warns_when_network_entry_distance_is_long(self): + _install_fake_freecad() + _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_label": "N1", + "network": { + "entry_distance": 125.0, + "exit_distance": 20.0, + }, + }, + { + "wire_label": "N2", + "network": { + "entry_distance": 20.0, + "exit_distance": 150.0, + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("接入距离提示:2 条导线", message) + self.assertIn("示例导线 N1", message) + self.assertIn("起点接入 125.0 mm", message) + def test_route_report_capacity_pressure_is_checked_per_route(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() @@ -3445,6 +4201,47 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual(1, route["network"]["carriers"]) self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + def test_route_eplan_connections_can_skip_nearer_isolated_entry_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, 1, 20), app.Vector(5, 1, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(1, report["routed"]) + self.assertEqual(0, len(report["errors"])) + route = report["routes"][0] + self.assertEqual("network-dijkstra-v1", route["algorithm"]) + self.assertGreater(route["network"]["entry_distance"], 1.0) + self.assertGreater(route["network"]["entry_candidate_rank"], 1) + def test_route_eplan_connections_report_includes_routing_path_network_diagnostic(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -4178,6 +4975,36 @@ class AutoRoutingTest(unittest.TestCase): self.assertIn("缺少布线路径网络 3 条", message) self.assertIn("请先生成线槽、布线面或布线路径网络", message) + def test_route_eplan_connections_report_includes_missing_route_network_sample(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 1, + "routed": 0, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 1, + "missing_route_network_samples": [ + { + "wire_uuid": "wire-1", + "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", + "error": "没有可用的布线路径网络:起点和终点无法连通", + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("缺路径网络示例:导线 N4111", message) + self.assertIn("QF1/A1 (terminal-start) -> KM1/13 (terminal-end)", message) + self.assertIn("原因:没有可用的布线路径网络:起点和终点无法连通", message) + def test_route_eplan_connections_report_includes_readable_missing_endpoint_labels(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() From 9d31edff70d69bab9e6474a62c261e0cd58572e7 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Thu, 4 Jun 2026 17:56:36 +0800 Subject: [PATCH 61/63] =?UTF-8?q?feat(freecad):=20=E4=BC=98=E5=8C=963D?= =?UTF-8?q?=E8=A3=85=E9=85=8D=E8=B4=B4=E5=90=88=E4=B8=8E=E6=A0=91=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=8E=92=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06-02-batch-din-device-placement-design.md | 144 ++++++++ src/Mod/FreeCADExchange/DeviceImport.py | 1 + src/Mod/FreeCADExchange/ManualWiringPanel.py | 199 +++++++++- src/Mod/FreeCADExchange/TerminalImport.py | 3 + src/Mod/FreeCADExchange/TerminalObjects.py | 40 +++ ...eecad_exchange_manual_wiring_panel_test.py | 340 +++++++++++++++++- .../freecad_exchange_terminal_objects_test.py | 20 ++ 7 files changed, 736 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md index d094f53..4cb2eed 100644 --- a/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md +++ b/docs/superpowers/specs/2026-06-02-batch-din-device-placement-design.md @@ -142,6 +142,144 @@ FreeCAD 导入工程端子时按下面顺序读取: 当前实现重点保证批量排布稳定、身份不丢失。复杂 Assembly Joint、端子片端挡、隔板、跨接片、短接片规则暂不纳入第一版。 +## 装配视频复盘 + +本节作为后续 3D 装配优化的对比基准。装配相关需求、问题复盘和验收差异优先沉淀到本文档,再拆分为具体实现计划。 + +### 用户装配视频提炼 + +用户视频前半段体现的目标流程: + +1. 先按真实设备和实物安装关系确认 3D 模型是否匹配。 +2. 对设备补充可复用的装配脚点、连接点或接线点。 +3. 设备脚点制作完成后,保存为可复用 `.FCStd` 模块。 +4. 后续工程中再次插入该设备时,自动带出脚点、端子和装配语义。 +5. 装配时可使用 FreeCAD 原生 `切换透明度`、`显示/隐藏所选`,便于选中柜板、导轨、线槽和设备背面。 +6. 按步骤导入设备并完成贴合,避免只靠人工拖拽。 + +当前 FreeCAD 二开需要重点解决的问题: + +- 面不容易选中,尤其是柜内导轨、线槽、设备背面被遮挡时。 +- 旋转模型后再贴合,容易出现一部分贴合、一部分穿模或悬空。 +- 贴合时如果只移动可视子对象,父对象 `Placement` 没同步,后续使用 `变换` 会回到旧位置。 +- 多选多个设备面参与贴合不合理,约束语义不清,会导致算法不知道哪个面是移动面。 +- 线槽、导轨贴合后仍需要能二次修改长度,并保持与柜板的贴合关系。 +- FreeCAD 任务面板和原生 `变换` 任务框会冲突,普通用户需要一键关闭当前面板并进入原生变换。 + +### 甲方视频参考能力 + +甲方视频中可参考的装配体验: + +- 柜体、导轨、线槽、安装板可透明显示,便于从柜内选择目标面;透明化优先复用 FreeCAD 原生右键菜单能力。 +- 对象树、属性面板和三维操纵器联动,用户能明确看到当前选中的对象和坐标。 +- 装配过程使用面、边、点作为参考,而不是单纯输入绝对坐标。 +- 设备沿导轨或安装板成组排列,位置规则清晰,适合端子排、断路器、继电器等电气元件。 +- 贴合后仍能继续微调距离、方向和局部偏移。 +- 电气装配关注柜板、导轨、线槽、设备安装面,不需要第一阶段实现完整机械 CAD 装配约束。 + +## 后续装配优化方向 + +后续装配能力优先向 SolidWorks Electrical / EPLAN 的电气柜装配体验靠拢,但第一阶段只做电气常用能力,不做完整机械装配工作台。 + +### 1. 面贴合可靠性 + +目标: + +- 目标面和移动面只允许一对一贴合。 +- 如果用户已点击 `设为贴合目标面`,后续只能再选一个移动面。 +- 如果用户一次选择两个面,按选择顺序解释为:第一个目标面,第二个移动面。 +- 如果选择超过两个面,直接提示重新选择,不执行贴合。 +- 贴合时同时更新父级可移动对象的 `Placement`,避免可视位置和对象坐标脱节。 + +贴合计算原则: + +```text +移动面法向 -> 目标面反向法向 +移动面参考点 -> 目标面参考点所在平面 +最终位姿写入可移动父对象 Placement +``` + +### 2. 旋转模型后的贴合 + +目标: + +- 用户为了选面临时旋转设备后,贴合仍能根据真实面法向计算旋转和位移。 +- 不再只做单轴平移。 +- 贴合完成后设备安装面应整体与目标面共面,不允许局部穿模。 +- 用户可设置 `贴合间距`,0 mm 表示完全贴合,正值表示沿目标面法向预留距离。 +- 已贴合对象保存 `QetMountHostNormalJson` 和 `QetMountOffsetMm`,后续选择对象后可点击 `应用贴合间距` 做二次调节。 +- 如果模型法向与现场直觉相反,选择已贴合对象后点击 `反转贴合方向`,再应用贴合间距。 + +验收: + +- 电流互感器、小型断路器、端子片旋转后,仍可贴到导轨或柜板。 +- 贴合后使用 FreeCAD 原生 `变换`,对象从当前贴合位置继续移动,不跳回旧位置。 +- 在 `3D手动布线` 面板中选择对象后点击 `关闭面板并变换`,系统先关闭当前任务面板,再调用 FreeCAD 原生 `Std_TransformManip`。 + +### 3. 导轨、线槽、柜板宿主语义 + +装配宿主分为: + +- `cabinet`:柜板、安装板、门板等。 +- `rail`:DIN 导轨。 +- `wire_duct`:线槽。 +- `device`:已经装配好的设备,可作为局部参考。 + +宿主对象应保存: + +- `QetCarrierKind` +- `QetCarrierAxis` +- `QetCarrierBaseLength` +- `QetMountMode` +- `QetMountHostName` +- `QetMountHostKind` +- `QetMountContactSubElement` +- `QetMountHostSubElement` +- `QetMountLocalBaseJson` +- `QetMountHostBaseJson` + +这些属性用于保存重开、刷新宿主装配和后续自动布线。 + +### 4. 长度二次调节 + +导轨和线槽长度调整规则: + +- 导入时可设置初始长度。 +- 贴合到柜板后仍可修改长度。 +- 修改长度时保持宿主贴合面不变。 +- 长度变化应优先沿 `QetCarrierAxis` 扩展。 +- 如果对象是导入的 FCStd/STEP 组合体,优先修改带 `QetCarrierBaseLength` 的父级载体对象,不应误选内部子零件。 + +### 5. 设备模板化与复用 + +设备模板应包含: + +- 真实几何模型。 +- 安装接触面或装配脚点。 +- 工程端子 LCS。 +- 端子出线方向。 +- 可选局部出线路径。 + +保存为 `.FCStd` 后,QET 再次导入同型号设备时,应复用这些模板语义。正式导线匹配仍以 QET 传入的 `terminal_uuid` 为准,不使用 `local:*` 作为正式端子身份。 + +### 6. 电气装配优先级 + +优先实现: + +1. 导轨贴柜板。 +2. 线槽贴柜板。 +3. 端子排沿导轨排列。 +4. 小型断路器沿导轨排列。 +5. 电流互感器、继电器等设备贴导轨或柜板。 +6. 贴合后的长度调节和刷新宿主装配。 + +暂不优先实现: + +- 完整机械装配 Joint。 +- 螺钉、孔、螺纹的精确机械配合。 +- 复杂运动学约束。 +- 完整 SW Mechanical 级别的 Mate 系统。 + ## UI 入口位于: @@ -154,6 +292,12 @@ QET模板 -> 3D手动布线 - `批量端子排` - `批量断路器` +- `设为贴合目标面` +- `贴合到选中面` +- `应用贴合间距` +- `反转贴合方向` +- `刷新宿主装配` +- `贴合间距` 参数窗口说明: diff --git a/src/Mod/FreeCADExchange/DeviceImport.py b/src/Mod/FreeCADExchange/DeviceImport.py index f9cbb4f..dc62731 100644 --- a/src/Mod/FreeCADExchange/DeviceImport.py +++ b/src/Mod/FreeCADExchange/DeviceImport.py @@ -1109,6 +1109,7 @@ def import_devices_from_payload(payload, scene_path=""): if not instance_id: report["imported_without_instance_id"] += 1 + TerminalObjects.sort_group_children(root_group) doc.recompute() try: Gui.SendMsgToActiveView("ViewFit") diff --git a/src/Mod/FreeCADExchange/ManualWiringPanel.py b/src/Mod/FreeCADExchange/ManualWiringPanel.py index fc5598e..55e15d7 100644 --- a/src/Mod/FreeCADExchange/ManualWiringPanel.py +++ b/src/Mod/FreeCADExchange/ManualWiringPanel.py @@ -409,7 +409,12 @@ def _selected_contact_face_refs(): shape_type = (getattr(sub_object, "ShapeType", "") or "").strip().lower() if shape_type != "face": continue - point = _face_anchor_point(picked, sub_object) + point = None + picked_points = list(getattr(picked, "PickedPoints", []) or []) + if index < len(picked_points): + point = picked_points[index] + if point is None: + point = _face_anchor_point(picked if not picked_points else None, sub_object) normal = _face_normal(sub_object) if point is None or normal is None: continue @@ -422,7 +427,6 @@ def _selected_contact_face_refs(): "subelement_name": subelement_names[index] if index < len(subelement_names) else "", } ) - break if refs: return refs @@ -464,6 +468,13 @@ def _has_placement(obj): return getattr(obj, "Placement", None) is not None +def _is_app_part_object(obj): + try: + return bool(obj is not None and obj.isDerivedFrom("App::Part")) + except Exception: + return (getattr(obj, "TypeId", "") or "") == "App::Part" + + def _contact_transform_object(obj): carrier = _carrier_object_from_object(obj) if carrier is not None and _has_placement(carrier): @@ -484,6 +495,7 @@ def _contact_transform_object(obj): name.startswith("QETDevice_") or (getattr(parent, "QetInstanceId", "") or "").strip() or (getattr(parent, "QetCarrierKind", "") or "").strip() + or _is_app_part_object(parent) ): best = parent current = parent @@ -502,12 +514,12 @@ def _rotation_for_face_contact(moving_normal, target_normal): return None -def _normal_contact_translation(target_point, target_normal, moving_point): +def _normal_contact_translation(target_point, target_normal, moving_point, offset_mm=0.0): normal = _normalize_vector(target_normal) if normal is None: return None signed_distance = _vector_dot(_vector_sub(moving_point, target_point), normal) - return _vector_scale(normal, -signed_distance) + return _vector_scale(normal, float(offset_mm or 0.0) - signed_distance) def _rotate_object_about_point(obj, rotation, pivot): @@ -645,7 +657,7 @@ def _mount_kind(obj): return "object" -def _set_face_contact_mount_metadata(moving_obj, target_ref, moving_ref): +def _set_face_contact_mount_metadata(moving_obj, target_ref, moving_ref, offset_mm=0.0): if moving_obj is None or not isinstance(target_ref, dict) or not isinstance(moving_ref, dict): return target_obj = target_ref.get("object") @@ -672,6 +684,12 @@ def _set_face_contact_mount_metadata(moving_obj, target_ref, moving_ref): "QET cabinet assembly mount metadata", value, ) + _ensure_float_property( + moving_obj, + "QetMountOffsetMm", + float(offset_mm or 0.0), + "QET cabinet assembly contact offset in target normal direction", + ) if target_base is not None: _set_vector_json_property( moving_obj, @@ -679,6 +697,14 @@ def _set_face_contact_mount_metadata(moving_obj, target_ref, moving_ref): target_base, "QET cabinet assembly host base at bind time", ) + host_normal = _normalize_vector(target_ref.get("normal")) + if host_normal is not None: + _set_vector_json_property( + moving_obj, + "QetMountHostNormalJson", + host_normal, + "QET cabinet assembly host face normal at bind time", + ) if target_base is not None and moving_base is not None: _set_vector_json_property( moving_obj, @@ -1381,8 +1407,9 @@ def _abort_transaction(doc, opened): class ManualWiringController: - def __init__(self, terminal_exit_length=DEFAULT_TERMINAL_EXIT_LENGTH): + def __init__(self, terminal_exit_length=DEFAULT_TERMINAL_EXIT_LENGTH, contact_offset_mm=0.0): self.terminal_exit_length = float(terminal_exit_length or 0.0) + self.contact_offset_mm = float(contact_offset_mm or 0.0) self.current_task = None self.contact_target_ref = None self.start_terminal = None @@ -1395,6 +1422,10 @@ class ManualWiringController: self.terminal_exit_length = max(float(value or 0.0), 0.0) return self.terminal_exit_length + def set_contact_offset(self, value): + self.contact_offset_mm = float(value or 0.0) + return self.contact_offset_mm + def mark_selected_carriers(self, carrier_kind): carrier_kind = (carrier_kind or "").strip() if carrier_kind not in CARRIER_ROLE_LABELS: @@ -1475,6 +1506,7 @@ class ManualWiringController: _activate_document(doc) def apply_length_to_selected_carriers(self, length_mm): + doc = _active_document() selected = _selected_carrier_objects() if not selected: raise ManualWiringPanelError("请先选择线槽或导轨对象。") @@ -1485,8 +1517,83 @@ class ManualWiringController: updated.append(_apply_carrier_length(carrier, length_mm)) if not updated: raise ManualWiringPanelError("所选对象不是已标记的线槽或导轨。") + refresh_mount_hosted_objects(doc) try: - _active_document().recompute() + doc.recompute() + except Exception: + pass + return updated + + def apply_contact_offset_to_selected_mounts(self, offset_mm): + doc = _active_document() + selected = _selection() + if not selected: + raise ManualWiringPanelError("请先选择已贴合的对象。") + + new_offset = float(offset_mm or 0.0) + updated = [] + for selected_obj in selected: + obj = _contact_transform_object(selected_obj) + if obj is None or (getattr(obj, "QetMountMode", "") or "").strip() != "face_contact": + continue + host_normal = _normalize_vector(_vector_from_json_property(obj, "QetMountHostNormalJson")) + if host_normal is None: + raise ManualWiringPanelError("所选对象没有保存贴合法向,请重新执行一次贴合后再调节间距。") + old_offset = float(getattr(obj, "QetMountOffsetMm", 0.0) or 0.0) + delta = _vector_scale(host_normal, new_offset - old_offset) + _translate_object(obj, delta) + _ensure_float_property( + obj, + "QetMountOffsetMm", + new_offset, + "QET cabinet assembly contact offset in target normal direction", + ) + host_name = (getattr(obj, "QetMountHostName", "") or "").strip() + host = doc.getObject(host_name) if host_name and hasattr(doc, "getObject") else None + host_base = _placement_base(host) + obj_base = _placement_base(obj) + if host_base is not None and obj_base is not None: + _set_vector_json_property( + obj, + "QetMountLocalBaseJson", + _vector_sub(obj_base, host_base), + "QET cabinet assembly local base offset from host", + ) + updated.append(obj) + if not updated: + raise ManualWiringPanelError("所选对象不是已贴合的装配对象。") + self.contact_offset_mm = new_offset + try: + doc.recompute() + except Exception: + pass + return updated + + def reverse_contact_normal_for_selected_mounts(self): + doc = _active_document() + selected = _selection() + if not selected: + raise ManualWiringPanelError("请先选择已贴合的对象。") + + updated = [] + for selected_obj in selected: + obj = _contact_transform_object(selected_obj) + if obj is None or (getattr(obj, "QetMountMode", "") or "").strip() != "face_contact": + continue + host_normal = _normalize_vector(_vector_from_json_property(obj, "QetMountHostNormalJson")) + if host_normal is None: + raise ManualWiringPanelError("所选对象没有保存贴合法向,请重新执行一次贴合后再反转方向。") + _set_vector_json_property( + obj, + "QetMountHostNormalJson", + _vector_scale(host_normal, -1.0), + "QET cabinet assembly host face normal at bind time", + ) + updated.append(obj) + if not updated: + raise ManualWiringPanelError("所选对象不是已贴合的装配对象。") + try: + doc.recompute() except Exception: pass return updated @@ -1523,11 +1630,12 @@ class ManualWiringController: target["point"], target["normal"], moving["point"], + self.contact_offset_mm, ) if translation is None: raise ManualWiringPanelError("无法读取目标面的法向,不能执行贴合。") _translate_object(moving_object, translation) - _set_face_contact_mount_metadata(moving_object, target, moving) + _set_face_contact_mount_metadata(moving_object, target, moving, self.contact_offset_mm) try: _active_document().recompute() except Exception: @@ -1541,6 +1649,7 @@ class ManualWiringController: "moving_point": moving["point"], "translation": translation, "translation_mode": "normal", + "contact_offset_mm": self.contact_offset_mm, "rotated": rotated, } @@ -1770,6 +1879,23 @@ class ManualWiringController: return True raise ManualWiringPanelError("当前 FreeCAD 文档不支持撤销。") + def launch_native_transform_for_selection(self): + _active_document() + if not _selection(): + raise ManualWiringPanelError("请选择要变换的对象。") + if Gui is None or not hasattr(Gui, "runCommand"): + raise ManualWiringPanelError("当前 FreeCAD 界面不支持原生变换命令。") + + # FreeCAD 原生变换也是任务面板;先关闭本面板,避免 TaskDialog 互相占用。 + control = getattr(Gui, "Control", None) + if control is not None and hasattr(control, "closeDialog"): + try: + control.closeDialog() + except Exception: + pass + Gui.runCommand("Std_TransformManip") + return True + def set_end_from_selection_and_generate(self): doc = _active_document() if self.start_terminal is None: @@ -1859,10 +1985,11 @@ class ManualWiringController: ) if len(self.waypoints) > 3: waypoint_text += ";..." - return "任务:{0};起点:{1};出线:{2:.1f} mm;折点:{3} 个;最近导线:{4};折点明细:{5}".format( + return "任务:{0};起点:{1};出线:{2:.1f} mm;贴合间距:{3:.1f} mm;折点:{4} 个;最近导线:{5};折点明细:{6}".format( task_text, start_text, self.terminal_exit_length, + self.contact_offset_mm, len(self.waypoints), wire_text, waypoint_text, @@ -1889,6 +2016,12 @@ class ManualWiringTaskPanel: self.exit_length_input.setSingleStep(5.0) self.exit_length_input.setSuffix(" mm") self.exit_length_input.setValue(self.controller.terminal_exit_length) + self.contact_offset_input = QtWidgets.QDoubleSpinBox() + self.contact_offset_input.setRange(-1000.0, 1000.0) + self.contact_offset_input.setDecimals(1) + self.contact_offset_input.setSingleStep(1.0) + self.contact_offset_input.setSuffix(" mm") + self.contact_offset_input.setValue(self.controller.contact_offset_mm) self.carrier_length_input = QtWidgets.QDoubleSpinBox() self.carrier_length_input.setRange(1.0, 10000.0) self.carrier_length_input.setDecimals(1) @@ -1906,7 +2039,10 @@ class ManualWiringTaskPanel: self.batch_breaker_button = QtWidgets.QPushButton("批量断路器") self.set_contact_target_button = QtWidgets.QPushButton("设为贴合目标面") self.align_faces_button = QtWidgets.QPushButton("贴合到选中面") + self.apply_contact_offset_button = QtWidgets.QPushButton("应用贴合间距") + self.reverse_contact_normal_button = QtWidgets.QPushButton("反转贴合方向") self.refresh_mount_hosts_button = QtWidgets.QPushButton("刷新宿主装配") + self.native_transform_button = QtWidgets.QPushButton("关闭面板并变换") self.waypoint_button = QtWidgets.QPushButton("添加折点") self.delete_waypoint_button = QtWidgets.QPushButton("删除最后折点") self.end_button = QtWidgets.QPushButton("设为终点并生成") @@ -1925,6 +2061,10 @@ class ManualWiringTaskPanel: exit_layout.addWidget(QtWidgets.QLabel("手动端子出线长度")) exit_layout.addWidget(self.exit_length_input) layout.addLayout(exit_layout) + contact_offset_layout = QtWidgets.QHBoxLayout() + contact_offset_layout.addWidget(QtWidgets.QLabel("贴合间距")) + contact_offset_layout.addWidget(self.contact_offset_input) + layout.addLayout(contact_offset_layout) carrier_length_layout = QtWidgets.QHBoxLayout() carrier_length_layout.addWidget(QtWidgets.QLabel("载体长度")) carrier_length_layout.addWidget(self.carrier_length_input) @@ -1945,7 +2085,10 @@ class ManualWiringTaskPanel: layout.addLayout(batch_layout) layout.addWidget(self.set_contact_target_button) layout.addWidget(self.align_faces_button) + layout.addWidget(self.apply_contact_offset_button) + layout.addWidget(self.reverse_contact_normal_button) layout.addWidget(self.refresh_mount_hosts_button) + layout.addWidget(self.native_transform_button) layout.addWidget(self.start_button) layout.addWidget(self.waypoint_button) layout.addWidget(self.delete_waypoint_button) @@ -1971,6 +2114,7 @@ class ManualWiringTaskPanel: self.use_task_button.clicked.connect(self.use_selected_task) self.reload_tasks_button.clicked.connect(self._refresh_task_list) self.exit_length_input.valueChanged.connect(self.set_exit_length) + self.contact_offset_input.valueChanged.connect(self.set_contact_offset) self.import_duct_button.clicked.connect(self.import_wire_duct) self.import_rail_button.clicked.connect(self.import_din_rail) self.apply_carrier_length_button.clicked.connect(self.apply_carrier_length) @@ -1981,7 +2125,10 @@ class ManualWiringTaskPanel: self.batch_breaker_button.clicked.connect(self.create_breakers) self.set_contact_target_button.clicked.connect(self.set_contact_target_face) self.align_faces_button.clicked.connect(self.align_selected_contact_faces) + self.apply_contact_offset_button.clicked.connect(self.apply_contact_offset) + self.reverse_contact_normal_button.clicked.connect(self.reverse_contact_normal) self.refresh_mount_hosts_button.clicked.connect(self.refresh_mount_hosts) + self.native_transform_button.clicked.connect(self.launch_native_transform) self.start_button.clicked.connect(self.set_start) self.waypoint_button.clicked.connect(self.add_waypoint) self.delete_waypoint_button.clicked.connect(self.delete_last_waypoint) @@ -2057,6 +2204,13 @@ class ManualWiringTaskPanel: except Exception as exc: self._set_error(str(exc)) + def set_contact_offset(self, value): + try: + self.controller.set_contact_offset(value) + self._set_status(self.controller.state_text()) + except Exception as exc: + self._set_error(str(exc)) + def _select_carrier_asset_path(self, carrier_kind): default_path = _builtin_carrier_asset_path(carrier_kind) default_dir = str(Path(default_path).parent) if default_path else "" @@ -2188,6 +2342,27 @@ class ManualWiringTaskPanel: except Exception as exc: self._set_error(str(exc)) + def apply_contact_offset(self): + try: + updated = self.controller.apply_contact_offset_to_selected_mounts( + self.contact_offset_input.value() + ) + self._set_status( + "已对 {0} 个贴合对象应用间距 {1:.1f} mm。".format( + len(updated), + self.contact_offset_input.value(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def reverse_contact_normal(self): + try: + updated = self.controller.reverse_contact_normal_for_selected_mounts() + self._set_status("已反转 {0} 个贴合对象的间距方向。".format(len(updated))) + except Exception as exc: + self._set_error(str(exc)) + def set_contact_target_face(self): try: target = self.controller.set_contact_target_from_selection() @@ -2203,6 +2378,12 @@ class ManualWiringTaskPanel: except Exception as exc: self._set_error(str(exc)) + def launch_native_transform(self): + try: + self.controller.launch_native_transform_for_selection() + except Exception as exc: + self._set_error(str(exc)) + def set_start(self): try: terminal = self.controller.set_start_from_selection() diff --git a/src/Mod/FreeCADExchange/TerminalImport.py b/src/Mod/FreeCADExchange/TerminalImport.py index 8b922a2..e603f6e 100644 --- a/src/Mod/FreeCADExchange/TerminalImport.py +++ b/src/Mod/FreeCADExchange/TerminalImport.py @@ -629,6 +629,9 @@ def import_terminals_from_payload(payload, scene_path=""): _hide_object(source_obj) report["reused_template_hints"] += 1 + TerminalObjects.sort_group_children(terminal_group) + + TerminalObjects.sort_group_children(root_group) doc.recompute() if Gui is not None: try: diff --git a/src/Mod/FreeCADExchange/TerminalObjects.py b/src/Mod/FreeCADExchange/TerminalObjects.py index f30d5f5..5f59081 100644 --- a/src/Mod/FreeCADExchange/TerminalObjects.py +++ b/src/Mod/FreeCADExchange/TerminalObjects.py @@ -3,6 +3,7 @@ import json import math import os +import re from pathlib import Path import FreeCAD as App @@ -45,6 +46,45 @@ def is_local_terminal_uuid(value): return (value or "").strip().lower().startswith("local:") +def natural_sort_key(value): + text = str(value or "").strip().casefold() + parts = re.split(r"(\d+)", text) + key = [] + for part in parts: + if not part: + continue + if part.isdigit(): + key.append((0, int(part))) + else: + key.append((1, part)) + return tuple(key) + + +def object_display_sort_key(obj): + label = (getattr(obj, "Label", "") or "").strip() + name = (getattr(obj, "Name", "") or "").strip() + return (natural_sort_key(label or name), natural_sort_key(name)) + + +def sort_group_children(group): + children = list(getattr(group, "Group", []) or []) + if len(children) < 2: + return children + sorted_children = sorted( + enumerate(children), + key=lambda item: (object_display_sort_key(item[1]), item[0]), + ) + ordered = [child for _index, child in sorted_children] + try: + group.Group = ordered + except Exception: + try: + group.Group[:] = ordered + except Exception: + return children + return ordered + + def ensure_string_property(obj, prop_name, group_name, description, value): if prop_name not in getattr(obj, "PropertiesList", []): obj.addProperty("App::PropertyString", prop_name, group_name, description) diff --git a/tests/python/freecad_exchange_manual_wiring_panel_test.py b/tests/python/freecad_exchange_manual_wiring_panel_test.py index 5ec5884..f483a6e 100644 --- a/tests/python/freecad_exchange_manual_wiring_panel_test.py +++ b/tests/python/freecad_exchange_manual_wiring_panel_test.py @@ -57,9 +57,15 @@ def _install_fake_freecad(): ) sys.modules["FreeCAD"] = fake_freecad - selection_state = {"selection": [], "selection_ex": []} + selection_state = { + "selection": [], + "selection_ex": [], + "commands": [], + "control_events": [], + } fake_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.addCommand = lambda *args, **kwargs: None + fake_freecadgui.runCommand = lambda command: selection_state["commands"].append(command) fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None def clear_selection(): selection_state["selection"] = [] @@ -76,7 +82,7 @@ def _install_fake_freecad(): fake_freecadgui.Control = types.SimpleNamespace( activeDialog=lambda: False, showDialog=lambda panel: panel, - closeDialog=lambda: None, + closeDialog=lambda: selection_state["control_events"].append("closeDialog"), ) sys.modules["FreeCADGui"] = fake_freecadgui @@ -643,6 +649,58 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual(400.0, getattr(carrier, "QetCarrierLength", None)) self.assertEqual(2.0, getattr(carrier, "QetCarrierScaleX", None)) + def test_controller_refreshes_face_contact_mount_when_changing_carrier_length(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + carrier = doc.addObject("App::DocumentObjectGroup", "WireDuctCarrier") + cabinet.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) + carrier.Placement = app.Placement(app.Vector(120, 0, 5), app.Rotation()) + terminal_objects.ensure_string_property( + carrier, + "QetCarrierKind", + "QET Wiring", + "Carrier kind", + "wire_duct", + ) + carrier.addProperty("App::PropertyFloat", "QetCarrierBaseLength", "QET Wiring", "Base length") + carrier.QetCarrierBaseLength = 200.0 + terminal_objects.ensure_string_property( + carrier, + "QetMountMode", + "QET Assembly", + "QET cabinet assembly mount metadata", + "face_contact", + ) + terminal_objects.ensure_string_property( + carrier, + "QetMountHostName", + "QET Assembly", + "QET cabinet assembly mount metadata", + "CabinetPanel", + ) + terminal_objects.ensure_string_property( + carrier, + "QetMountLocalBaseJson", + "QET Assembly", + "QET cabinet assembly local base offset", + json.dumps({"x": 20.0, "y": 0.0, "z": 5.0}, ensure_ascii=False), + ) + cabinet.Placement = app.Placement(app.Vector(130, 0, 0), app.Rotation()) + selection_state["selection"] = [carrier] + + updated = panel.ManualWiringController().apply_length_to_selected_carriers(500.0) + + self.assertEqual([carrier], updated) + self.assertEqual(500.0, carrier.QetCarrierLength) + self.assertEqual((150.0, 0.0, 5.0), (carrier.Placement.Base.x, carrier.Placement.Base.y, carrier.Placement.Base.z)) + self.assertEqual({"x": 130.0, "y": 0.0, "z": 0.0}, json.loads(carrier.QetMountHostBaseJson)) + def test_controller_auto_marks_selected_wire_duct_by_name_before_length_change(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() @@ -797,6 +855,160 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual("Face2", rail.QetMountContactSubElement) self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, json.loads(rail.QetMountHostBaseJson)) self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountLocalBaseJson)) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountHostNormalJson)) + + def test_controller_aligns_contact_faces_with_configured_normal_offset(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(5, 6, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=rail, + ), + ] + + result = panel.ManualWiringController(contact_offset_mm=2.0).align_selected_contact_faces() + + self.assertIs(rail, result["moving_object"]) + self.assertEqual((0.0, 0.0, 3.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) + self.assertEqual((-0.0, -0.0, -7.0), (result["translation"].x, result["translation"].y, result["translation"].z)) + self.assertEqual(2.0, result["contact_offset_mm"]) + self.assertEqual(2.0, rail.QetMountOffsetMm) + + def test_controller_applies_contact_offset_to_selected_mounted_object(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(5, 6, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=rail, + ), + ] + controller = panel.ManualWiringController() + controller.align_selected_contact_faces() + selection_state["selection"] = [rail] + + updated = controller.apply_contact_offset_to_selected_mounts(5.0) + + self.assertEqual([rail], updated) + self.assertEqual((0.0, 0.0, 6.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) + self.assertEqual(5.0, rail.QetMountOffsetMm) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 6.0}, json.loads(rail.QetMountLocalBaseJson)) + + def test_controller_reverses_contact_normal_for_selected_mounted_object(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(100, 20, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(5, 6, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=rail, + ), + ] + controller = panel.ManualWiringController() + controller.align_selected_contact_faces() + selection_state["selection"] = [rail] + + reversed_objects = controller.reverse_contact_normal_for_selected_mounts() + updated = controller.apply_contact_offset_to_selected_mounts(5.0) + + self.assertEqual([rail], reversed_objects) + self.assertEqual([rail], updated) + self.assertEqual({"x": -0.0, "y": -0.0, "z": -1.0}, json.loads(rail.QetMountHostNormalJson)) + self.assertEqual((0.0, 0.0, -4.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) + + def test_controller_requires_saved_contact_normal_before_applying_offset(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + rail = doc.addObject("Part::Feature", "DINRail") + rail.Placement = app.Placement(app.Vector(0, 0, 1), app.Rotation()) + terminal_objects.ensure_string_property( + rail, + "QetMountMode", + "QET Assembly", + "QET cabinet assembly mount metadata", + "face_contact", + ) + selection_state["selection"] = [rail] + + with self.assertRaisesRegex(panel.ManualWiringPanelError, "贴合法向"): + panel.ManualWiringController().apply_contact_offset_to_selected_mounts(3.0) def test_refresh_mount_hosted_objects_moves_child_by_host_delta(self): _install_fake_freecad() @@ -999,6 +1211,97 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual((0.0, 0.0, 0.0), (child.Placement.Base.x, child.Placement.Base.y, child.Placement.Base.z)) self.assertEqual([device], selection_state["selection"]) + def test_controller_rejects_multiple_moving_faces_after_target_face_is_set(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + rail = doc.addObject("Part::Feature", "DINRail") + + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face_a = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + moving_face_b = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + controller = panel.ManualWiringController() + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ) + ] + controller.set_contact_target_from_selection() + + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 9), app.Vector(0, 1, 9)], + SubObjects=[moving_face_a, moving_face_b], + SubElementNames=["Face2", "Face3"], + Object=rail, + ) + ] + + with self.assertRaisesRegex(panel.ManualWiringPanelError, "只选择一个"): + controller.align_selected_contact_faces() + + def test_controller_moves_plain_app_part_parent_when_selected_face_belongs_to_child_shape(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + cabinet = doc.addObject("Part::Feature", "CabinetPanel") + assembly = doc.addObject("App::Part", "ImportedFcstdDevice") + child = doc.addObject("Part::Feature", "ImportedFcstdBody") + assembly.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) + child.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) + assembly.addObject(child) + + target_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, 1), + ) + moving_face = types.SimpleNamespace( + ShapeType="Face", + normalAt=lambda u, v: app.Vector(0, 0, -1), + ) + selection_state["selection_ex"] = [ + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 0)], + SubObjects=[target_face], + SubElementNames=["Face1"], + Object=cabinet, + ), + types.SimpleNamespace( + PickedPoints=[app.Vector(0, 0, 9)], + SubObjects=[moving_face], + SubElementNames=["Face2"], + Object=child, + ), + ] + + result = panel.ManualWiringController().align_selected_contact_faces() + + self.assertIs(assembly, result["moving_object"]) + self.assertEqual((0.0, 0.0, 1.0), (assembly.Placement.Base.x, assembly.Placement.Base.y, assembly.Placement.Base.z)) + self.assertEqual((0.0, 0.0, 0.0), (child.Placement.Base.x, child.Placement.Base.y, child.Placement.Base.z)) + self.assertEqual([assembly], selection_state["selection"]) + def test_controller_requires_two_faces_for_contact_alignment(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() @@ -1558,6 +1861,39 @@ class ManualWiringPanelTest(unittest.TestCase): self.assertEqual(("undo", ""), doc.transactions[-1]) + def test_controller_closes_panel_and_launches_native_transform(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + device = doc.addObject("Part::Feature", "Device") + selection_state["selection"] = [device] + + result = panel.ManualWiringController().launch_native_transform_for_selection() + + self.assertTrue(result) + self.assertEqual(["closeDialog"], selection_state["control_events"]) + self.assertEqual(["Std_TransformManip"], selection_state["commands"]) + + def test_controller_requires_selection_before_native_transform(self): + selection_state = _install_fake_freecad() + terminal_objects, panel = _reload_modules() + app = sys.modules["FreeCAD"] + + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + selection_state["selection"] = [] + + with self.assertRaisesRegex(panel.ManualWiringPanelError, "请选择要变换的对象"): + panel.ManualWiringController().launch_native_transform_for_selection() + + self.assertEqual([], selection_state["control_events"]) + self.assertEqual([], selection_state["commands"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/python/freecad_exchange_terminal_objects_test.py b/tests/python/freecad_exchange_terminal_objects_test.py index dcce4af..83504fd 100644 --- a/tests/python/freecad_exchange_terminal_objects_test.py +++ b/tests/python/freecad_exchange_terminal_objects_test.py @@ -42,6 +42,7 @@ def _install_fake_freecad(): class FakeObject: def __init__(self, name, type_id="App::DocumentObjectGroup"): self.Name = name + self.Label = name self.TypeId = type_id self.Group = [] self.InList = [] @@ -131,5 +132,24 @@ class TemplateTerminalVisibilityTest(unittest.TestCase): self.assertTrue(engineering_terminal.ViewObject.Visibility) +class GroupSortingTest(unittest.TestCase): + def test_sort_group_children_uses_case_insensitive_natural_label_order(self): + _install_fake_freecad() + terminal_objects = _reload_module() + + root = FakeObject("QETExchangeDevices") + for label in ["ID:10", "TAa", "id:2", "TAb", "C - 电容柜001", "ID:7"]: + child = FakeObject("QETDevice_" + label) + child.Label = label + root.addObject(child) + + sorted_children = terminal_objects.sort_group_children(root) + + self.assertEqual( + ["C - 电容柜001", "id:2", "ID:7", "ID:10", "TAa", "TAb"], + [child.Label for child in sorted_children], + ) + + if __name__ == "__main__": unittest.main() From 3615f6244257eb4378d8de95558753c3d30303d2 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Tue, 9 Jun 2026 18:02:12 +0800 Subject: [PATCH 62/63] =?UTF-8?q?feat(freecad):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E4=B8=8E=E8=AF=8A=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/2D-3D交换协议.md | 5 + docs/FreeCAD 3D自动布线设计方案.md | 359 +- docs/FreeCAD 机柜装配操作文档.md | 253 +- .../2026-05-28-电气自动布线设计.md | 2 +- ...期3D功能任务拆解与开发顺序.md | 848 + src/Mod/FreeCADExchange/AutoRouting.py | 8367 +++++++- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 3127 ++- src/Mod/FreeCADExchange/ExchangeBootstrap.py | 78 + src/Mod/FreeCADExchange/RoutingNetwork.py | 2032 +- .../freecad_exchange_auto_routing_test.py | 16827 +++++++++++++--- .../freecad_exchange_bootstrap_wiring_test.py | 127 + 11 files changed, 27571 insertions(+), 4454 deletions(-) create mode 100644 docs/三期3D功能任务拆解与开发顺序.md diff --git a/docs/2D-3D交换协议.md b/docs/2D-3D交换协议.md index 9418b4b..53cfdba 100644 --- a/docs/2D-3D交换协议.md +++ b/docs/2D-3D交换协议.md @@ -333,6 +333,11 @@ - 不再混入几何 `Conductor` UUID 作为导线主标识 - `wire_style_id` 只取 `start_terminal` 所连接导线的样式 +- 如果 FreeCAD 需要直接渲染导线颜色/线宽,可在顶层额外提供 `wire_style_database_path`,指向包含 `wire_properties` 表的项目 SQLite 数据库;FreeCAD 会按 `wires[].wire_style_id -> wire_properties.id` 查询样式。该字段是可选字段,也可以通过环境变量 `QET_WIRE_PROPERTIES_DB` 提供。 +- 如果顶层没有提供数据库路径,FreeCAD 导入 `2d_to_3d.json` 时会尝试扫描 JSON 同目录下的 `.sqlite / .sqlite3 / .db` 文件;只有确认其中存在 `wire_properties` 表时,才会自动使用该库作为 `wire_style_database_path`,并在 `_qet_exchange_summary.wire_style_database_path`、批量布线 report 与 compact 诊断中记录最终路径。这是 FreeCAD 侧便利推断,不是 QET 必填输出字段。 +- 当前 FreeCAD 会读取 `wire_properties.line_color / line_width / diameter_mm / line_type / area_or_spec` 做第一版显示渲染;颜色支持 `#RRGGBB`、`RRGGBB`、`0xRRGGBB`、`#AARRGGBB`、`0xAARRGGBB`、十进制颜色整数、`rgb(...)`、逗号 RGB 和常见英文色名,ARGB 的 alpha 暂不参与线颜色。显示线宽优先使用 `line_width`,缺失时用 `diameter_mm`,两者都缺失时会尝试从 `area_or_spec` 的 `2.5mm2 / 2.5mm^2 / 2.5mm²` 等截面积文本估算。 +- 查到样式后,FreeCAD 会把常用样式字段展开成 3D 导线对象属性:`QetWireStyleName`、`QetWireSpecText`、`QetWireColorText`、`QetWireLineType`、`QetWireType`、`QetWireFormat`、`QetWireDiameterMm`、`QetWireLineWidth`;这些是 3D 侧查看/调试属性,不是 QET 必填输出字段。 +- FreeCAD 会在生成导线对象上写入 `QetWireStyleStatus=Resolved/Missing`,并同步写入 `QetRouteDiagnosticsJson.wire_style_status`,用于判断 `wire_style_id` 是否成功回查到 `wire_properties`;批量报告会汇总 `wire_style_status_counts`,已解析样式会进入批量 `routes[].wire_style` 和 compact `route_samples[].wire_style`,compact 诊断的 `missing_wire_style_samples[]` 会列出缺失样式样例,`route_samples[]` 会保留 `wire_style_id` 和 `wire_style_status`。这是 3D 侧诊断结果,不是 QET 必填输出字段。 - 不按整条几何路径聚合多个样式 - `wires` 是交换 JSON 的扩展层 diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 1307203..2882b4e 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -126,6 +126,45 @@ terminal_uuid 导线规格、颜色、线耳等导线数据由 QET 提供,FreeCAD 第一版只消费和保留,不在 3D 侧重新发明导线主数据。 +#### 1.4.4 空机柜和未装配状态的处理 + +自动布线必须基于 FreeCAD 文档中的真实 3D 位姿。左侧树目录中存在设备、线槽或导轨,只表示对象已经导入,并不表示它们已经装配到机柜内,也不表示已经形成可走线的路径网络。 + +因此,下面状态不能用来判断自动布线最终效果: + +1. 设备、端子排、小型断路器仍停留在导入位置,尚未摆到导轨或安装板上。 +2. 线槽、导轨尚未贴到机柜背板或安装板。 +3. 安装板、柜内空间或柜体没有作为柜内边界/布线区域参与识别。 +4. 没有生成 `WireDuct`、`RoutingRange`、`UserPath` 或 `TerminalAccess` carrier。 + +当前 FreeCAD 自动布线预检会把这些前置状态显式报告出来: + +```text +路径网络:0 段 +布线源:未识别到线槽/布线面/用户路径 +柜内边界:未标记 +``` + +点击自动布线面板中的 `检查布线准备度` 后,FreeCAD 还会在树目录 `QETWiring_05_Diagnostics` 下写入一个 `RoutingPreflight` 诊断对象。该对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示本次诊断是否通过;`QetDiagnosticIssueCodes` 直接列出问题码;`QetDiagnosticIssueLabels` 直接列出中文问题标签;`QetDiagnosticMessage` 保存中文摘要;`QetDiagnosticJson` 保存压缩后的最新预检结果,包括导线任务数量、工程端子数量、路径网络段数、布线源摘要、柜内边界摘要、导线样式库状态、问题码 `issue_codes`、缺失端点样例等。重复检查时旧的 `RoutingPreflight` 会被替换,只保留最新一次结果。 + +`RoutingPreflight` 还会附带 compact 路径网络诊断。若已标记 `CabinetInterior`,但主路径 carrier 或工程端子越出柜内边界,预检报告会直接追加 `route_carriers_outside_boundary` 或 `terminals_outside_boundary`,并在中文摘要中给出“越界路径”或“越界端子”样例。这样用户在生成导线前就能发现装配态问题。 + +预检的端点缺失示例会同时显示导线标签和端子对,例如 `导线 N4111,terminal-start -> terminal-missing`。这用于第一时间判断问题来自哪条 QET 导线任务、哪个端子 UUID 没有绑定到 FreeCAD 工程端子。 + +看到这类提示时,应先完成最小装配闭环,再测试布线结果: + +```text +安装板/背板 + -> 导轨 + -> 线槽或用户主路径 + -> 真实 QET 设备实例 + -> 工程端子 + -> 布线路径网络 + -> 自动布线连接 +``` + +如果现场本来没有线槽,也需要用草图、Draft 线或线段定义柜内主路径,并点击 `选中路径作为用户路径`,让它生成 `UserPath` carrier。否则算法没有可走的主网络,只能报告缺少路径网络。 + ### 1.5 设备脚号与 3D 脚点绑定方案 设备脚号与 3D 脚点绑定分成两层:模板槽位绑定和工程端子绑定。 @@ -199,6 +238,37 @@ QET 侧还需要保证导线任务中继续提供: 其中导线规格、颜色、线耳等导线主数据以后由 `wire_style_id` 或等价字段回查 QET。 +当前自动布线已经支持第一版导线样式渲染:`wires[].wire_style_id` 对应 QET 项目数据库 `wire_properties.id`。FreeCAD 可通过自动布线 options 中的 `wire_style_database_path`、`2d_to_3d.json` 顶层 `wire_style_database_path`,或环境变量 `QET_WIRE_PROPERTIES_DB`,打开项目 SQLite 数据库并查询 `wire_properties`。查询时优先匹配当前 `project_uuid`,读取到的样式会保存到已生成导线对象的 `QetWireStyleJson` 和 `QetRouteDiagnosticsJson.wire_style`。 + +如果 `2d_to_3d.json` 没有显式提供数据库路径,FreeCAD 导入交换文件时会尝试扫描 JSON 同目录下的 `.sqlite / .sqlite3 / .db` 文件;只有确认数据库中存在 `wire_properties` 表时,才会自动写入 `wire_style_database_path`。导入完成后,`_qet_exchange_summary.wire_style_database_path` 会记录最终使用的路径;自动布线面板摘要、批量布线 report、compact 诊断和中文报告都会显示“导线样式库:<路径>”,便于检查 FreeCAD 是否识别到样式库。这是 FreeCAD 侧便利推断,不要求 QET 修改输出协议。 + +第一版使用字段如下: + +```text +wire_properties.id -> wires[].wire_style_id +wire_properties.name -> 样式名称,写入诊断 +wire_properties.line_color -> FreeCAD 导线显示颜色,支持 #RRGGBB / RRGGBB / 0xRRGGBB / #AARRGGBB / 0xAARRGGBB / 十进制颜色整数 / rgb(...) / 逗号 RGB / 常见英文色名;ARGB 的 alpha 暂不参与线颜色 +wire_properties.line_type -> FreeCAD 导线线型,支持 Solid / DashLine / DotLine / DashDotLine 等常见写法 +wire_properties.line_width -> FreeCAD 视图线宽,优先使用 +wire_properties.diameter_mm -> line_width 缺失时作为显示线宽回退 +wire_properties.area_or_spec -> 导线规格文本,写入诊断;当 line_width 和 diameter_mm 都缺失时,支持从 2.5mm2 / 2.5mm^2 / 2.5mm² 等文本估算显示线宽 +``` + +查到样式后,FreeCAD 除了保存完整 `QetWireStyleJson`,还会把常用字段展开到导线对象属性,方便在 FreeCAD 属性面板中直接查看: + +```text +QetWireStyleName -> 样式名称 +QetWireSpecText -> 导线规格文本 +QetWireColorText -> 原始颜色文本 +QetWireLineType -> 原始线型文本 +QetWireType -> 导线类型 +QetWireFormat -> 导线格式 +QetWireDiameterMm -> 直径 +QetWireLineWidth -> 显示线宽 +``` + +如果查不到样式,导线仍按默认蓝色显示,并在导线对象 `QetWireStyleStatus` 与 `QetRouteDiagnosticsJson.wire_style_status` 中写入 `Missing`;查到样式时写入 `Resolved`。批量布线报告会汇总 `wire_style_status_counts`,中文报告会提示“导线样式:缺失 N 条”,并带上第一条缺失样例,例如“示例导线 N2 样式 404”。已解析样式会进入批量 `routes[].wire_style` 和 compact `route_samples[].wire_style`,compact 诊断也会输出 `missing_wire_style_samples[]`,`route_samples[]` 会保留 `wire_style_id` 和 `wire_style_status`,方便定位是哪条线缺少样式。这个状态由 FreeCAD 根据查询结果生成,不要求 QET 新增字段。如果导线存在碰撞告警,颜色仍会被红色告警覆盖,但线宽可以继续使用样式值。当前这是“视图线宽/颜色”的渲染,不是带真实半径的 3D 圆管导线。 + ### 1.6 区域与批量排布方案 #### 1.6.1 设备区域/柜内区域 @@ -303,14 +373,14 @@ QetTerminalUuid = local:: QetTerminalBindingMode = local ``` -这类端子只有空间槽位,没有 2D 电气语义,不能直接用于批量导线任务。正式布线前需要执行“检查/绑定工程端子”。系统会根据导线任务中的 `start/end_terminal_uuid`、`start/end_instance_id`、`start/end_element_uuid` 和端子显示号,在对应 3D 设备下查找 local 端子或模板槽位;匹配成功后写入: +这类端子只有空间槽位,没有 2D 电气语义,不能直接用于批量导线任务。正式布线前需要执行“检查/绑定工程端子”。系统会优先根据导线任务中的 `start/end_terminal_uuid`、`start/end_element_uuid` 和端子显示号,在对应 3D 设备下查找 local 端子或模板槽位;如果任务或 `devices[]` 中已有 `instance_id`,只作为 FreeCAD 侧辅助定位信息使用,不作为第一版 QET `wires[]` 的必填字段。匹配成功后写入: ```text QetTerminalUuid = QetTerminalBindingMode = qet ``` -如果导线任务缺少实例定位信息,或 QET 端子显示号与模板槽位名称不一致,绑定会跳过并给出诊断。 +如果导线任务缺少 `element_uuid`、找不到对应 3D 设备,或 QET 端子显示号与模板槽位名称不一致,绑定会跳过并给出诊断。 ### 3.2 路由路径 Carrier @@ -330,9 +400,11 @@ Points = [Vector, Vector, ...] QetRouteSourceName = QetRouteSourceLabel = QetRouteSourceKind = "WireDuct" | "RoutingRange" | "WiringCutOut" | "UserPath" | "TerminalAccess" +QetRouteSourcePathIndex = "1" | "2" | ... ``` 这些属性只用于 FreeCAD 文档内部刷新和清理,不写入数据库,也不要求 QET 提供。 +其中 `QetRouteSourcePathIndex` 主要用于同一个草图拆成多条 `UserPath` 时区分第几条源路径,便于诊断和路径示例回溯。只有同一源对象生成多条 `UserPath` 时才保留该序号;草图刷新后只剩单条路径时会清空旧序号。最终导线的 `QetRouteTrackJson` segment carrier payload 和公开 `carrier_payload()` 都会同步输出 `source_path_index`,便于导出诊断 JSON 后直接定位源草图路径。批量布线中文报告的“路径示例”在存在序号时会显示为 `源路径标签(路径1)`、`源路径标签(路径2)` 这类格式;普通单路径不写该字段,因此不会显示 `(路径1)`。 carrier 统一放在: @@ -543,19 +615,125 @@ QetWiringCutOutBridgeExtensionMm = 20.0 生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象,route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。route track 同时记录 carrier 的 `capacity`,用于后续核对多根线共路、容量偏好和绕行行为。 +生成导线对象的树目录 Label 会尽量包含导线标识、起终点端子和状态,例如 `N4111: terminal-start -> terminal-end (Routed)` 或 `N4111: terminal-start -> terminal-end (CollisionWarning)`。这里的端点优先使用 FreeCAD 端子对象 Label;在 QET 导入端子中通常就是 `terminal_uuid`。这样手动测试截图时,可以直接从树目录定位是哪条 QET 导线、哪两个端子以及当前状态。 + +单根导线对象会写入 `QetRouteIssueCodes` 和 `QetRouteIssueLabels`,用于汇总这根线自身的问题。当前会把长接入、碰撞/安全间隙、路径兜底、容量压力、柜内越界和候选入口碰撞风险映射成与批量诊断一致的问题码,例如 `long_terminal_access`、`collision_warnings`、`route_quality_warnings`、`route_capacity_pressure`、`route_candidate_boundary_violations`。`QetRouteDiagnosticsJson.issue_codes` 和 `issue_labels` 会保存同一份数组,便于导出 JSON 后按单线筛选。 + +为了让手动测试不用每次展开 `QetRouteTrackJson`,单根导线对象会同步写入 `QetRouteSourceLabels` 和 `QetRouteCarrierNames`。`QetRouteSourceLabels` 优先显示源对象标签,例如线槽、黄色草图路径或用户路径标签;同一个源草图拆成多条 `UserPath` 时会显示为 `源路径标签(路径2)` 这类格式。`QetRouteCarrierNames` 保留实际经过的 carrier 对象名,便于在树目录中进一步定位。完整数组也会写入 `QetRouteDiagnosticsJson.route_source_labels` 和 `QetRouteDiagnosticsJson.route_carrier_names`。 + 如果导线实际走过自动桥接边,`QetRouteTrackJson` 中对应段会记录 `is_bridge=true`,并汇总 `bridged_segments`。批量布线报告和诊断对象中的 route sample 会优先使用这个本路线实际桥接数量;旧诊断缺少该字段时,再回退到整张路径网络的桥接数量。自动桥接段是虚拟连通边,不代表真实线槽截面,因此不参与容量最小值计算,也不参与共路 lane 计数、路径复用惩罚、真实 carrier 类型汇总、诊断样例 carrier 列表和路径质量提示。 -批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。路径示例会跳过 `is_bridge=true` 的虚拟桥接段,避免把自动补出来的连通边误显示成真实线槽或用户路径。 +批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。如果某个 carrier 没有 `source_label` / `source_name`,路径示例会回退显示 carrier 自身的 `label` / `name`,避免手动创建或旧版本 carrier 完全无法定位。路径示例会跳过 `is_bridge=true` 的虚拟桥接段,避免把自动补出来的连通边误显示成真实线槽或用户路径。 批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻/投影主路径自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。 -一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长等问题带出来。 +中文报告会区分 `布线布局空间` 和 `当前路径网络`。前者表示本次操作中新生成或刷新的 carrier 数量,因此已有线槽路径复用时可能显示线槽路径 0 条;后者来自实际构建的路径网络 `route_network_carrier_kind_counts`,会显示当前参与求路的 `WireDuct / WireDuctOpenEnd / UserPath / TerminalAccess / RoutingRange` 等总数。判断是否识别到线槽或用户路径时,以 `当前路径网络` 和 `路径采用` 为准。 + +`RoutingConnectionBatch.QetDiagnosticJson.route_samples[]` 会保留少量导线样例。每个 route sample 除了基础导线、端子、路径来源和 network 数据外,还会同步写入 `access`、`collision_summary`、`quality`、`capacity`、`boundary` 等状态分组,字段口径与单根导线对象属性一致。这样手动测试后即使不逐根选中导线,也可以直接从诊断对象 JSON 看出样例线是否存在长接入、穿模/安全间隙、路径兜底、容量压力或柜内越界。 + +每个 route sample 还会保留 `wire_object_label`,其值与 FreeCAD 左侧树目录中生成导线对象的 Label 一致,例如 `N4111: terminal-start -> terminal-end (CollisionWarning)`。当用户把诊断 JSON 发回开发侧时,可以用这个字段直接对应到树目录里的导线对象,减少反复按 UUID 查找。 + +各类 warning sample 也会尽量保留 `wire_object_label`,包括接入距离、路径质量、候选入口碰撞风险、柜内边界、路径约束、容量压力和碰撞样例。这样不论问题来自哪一类诊断,都可以用同一个字段回到 `QETWiring_04_Routed` 下定位导线。 + +中文报告会区分“定位类示例”和“统计类提示”:碰撞示例、缺失端点示例优先显示 `wire_object_label`,方便在 FreeCAD 树目录中直接查找;导线样式缺失、路径示例、总量统计等仍使用短导线号,避免一行报告被完整对象 Label 拉得过长。 + +`route_samples[]` 不是简单截取前几条导线,而是优先保留带 `issue_codes` 的问题路线;问题数量相同或没有问题时,再按原生成顺序保留。这样当一次布线有很多正常线、少量异常线时,压缩诊断对象仍会优先给出异常样例,避免手动测试复制 JSON 后看不到真正需要处理的导线。 + +一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中,并会按诊断建议先生成必要的 `UserPath` 桥接。脚本或调试场景直接调用 `route_eplan_connection_tasks()` 时,也会先执行同一类诊断桥接,保证任务入口和面板入口都优先尝试把孤立线槽接入端子主网络。直接从 QET payload 生成批量布线时,如果发现导线已经生成但没有使用线槽、`UserPath` 或过线孔主路径,也会自动补一次路径网络诊断,并把线槽未接入端子主网络、桥接建议等根因写回同一份批量报告。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长、端子越出柜内边界、路径越出柜内边界等问题带出来。如果路径源本身越出 `CabinetInterior`,批量报告会额外显示“越界路径:<路径标签> N 个越界点”,便于直接定位错误的线槽中心线或 `UserPath`。如果工程端子越出边界,批量报告会显示“越界端子:<端子对象/UUID> N 个越界点”,便于直接定位未装配到柜内的设备端子。 + +真实工程中路径 carrier 数量可能达到数百个,入口候选组合会直接影响批量布线耗时。第一版保留单根布线的 `network_entry_candidate_limit`,同时在批量布线中增加 `batch_network_entry_candidate_limit`,默认按更保守的候选数求路,避免 `入口候选 x 出口候选 x 导线数量` 过度放大。批量入口候选还增加了总量保护 `batch_network_entry_total_candidate_limit`,当前默认值为 6;它会限制单根导线最终参与组合评分的入口/出口候选总量,避免“距离候选 + 柜内候选 + 避障候选”叠加后把一次布线放大成几十次 Dijkstra 求路。缺路径重试仍可以按 `missing_route_retry_candidate_limit` 临时放宽候选数量,但正常批量路径优先受总上限保护。批量布线还会复用本次已构建的基础路径图,避免每根导线重复构建同一套网络;碰撞障碍物也会先收集成候选缓存,再按每根导线的端点设备和端点附近规则过滤,避免重复扫描数千个模型对象。当前批量默认采用性能优先的 `batch_avoid_obstacles=false`:不额外构建障碍过滤图,但仍会在生成后做碰撞诊断并输出 `collision_warnings`;需要更激进避障时再开启批量障碍过滤。相关参数会写入 `RoutingConnectionBatch.QetDiagnosticJson.batch_network_entry_candidate_limit`、`batch_network_entry_total_candidate_limit`、`batch_avoid_obstacles` 和 `batch_obstacle_candidates`,便于手测时确认当前性能保护是否生效。 + +线槽接入主网络采用保守桥接策略。当前 `adjoining_duct_tolerance` 默认只允许 5mm 内的相邻端点或端点到主路径中段投影自动桥接,不会为了让线槽被使用而把远距离线槽强行接到布线面或端子接入网络。这样可以避免误把柜内无关路径连成一个错误网络。若诊断出现 `wire_ducts_without_terminal_access / 线槽未接入端子主网络`,第一版推荐用户显式添加 UserPath、线槽开口或桥接路径;诊断会在 `bridge_suggestion` 中给出建议连接的两段 carrier、最近点和距离。面板已提供 `按诊断建议生成桥接`,用于先刷新诊断再按明确建议生成桥接;也提供 `选中两路径生成桥接`,用于在用户选中的两个路径 carrier 最近点之间生成一段 `UserPath`。这两个能力都属于半自动路径网络编辑,不会扫描全柜并自动连接所有远距离线槽。对于 UserPath 端点正好落在线槽中段的 0mm 接入,路径图会把被接入的线槽段在该点切开并并网,避免视觉上已经接触但路径组件仍被诊断为孤立。 + +孤立路径网络诊断只针对可行动的路径组件。线槽、UserPath、过线孔、辅助路径和端子接入如果分成多个组件,会继续输出 `isolated_network_components`;但纯 `RoutingRange` 布线面孤岛只作为兜底网格保留在 `components` 明细中,不再单独触发“存在孤立路径网络”问题码。这样可以避免真实工程中安装板/布线面网格被误当作主路径断网问题,手测时优先处理线槽、用户路径和端子局部接入。 + +端子接入过长属于质量告警,不等同于路径断开。`terminal_access_max_distance` 控制是否允许生成端子接入;`terminal_access_warning_distance` 只控制超过多长时提示 `long_terminal_accesses`。当该值为 0 时继续沿用默认自动阈值;在较大机柜或端子到线槽本来就有较长局部出线的场景,可以把警告距离设置为 700mm 等工程可接受值,以减少误报,同时仍保留最大接入距离作为硬限制。 + +为了减少手动测试时反复展开属性查 JSON,`3D 布线连接` 面板提供 `汇总布线诊断`。它读取 FreeCAD 文档中最新的 `RoutingPreflight`、`RoutingPathNetwork`、`RoutingConnectionBatch` 三类诊断对象,按诊断类型合并状态、中文消息和 `issue_codes`,输出一个总的“通过/未通过”摘要,并刷新 `RoutingDiagnosticSummary` 对象。所有这类诊断对象都会把问题码同步写到 `QetDiagnosticIssueCodes`,把中文问题标签同步写到 `QetDiagnosticIssueLabels`,方便在属性面板中直接查看;完整明细仍保留在 `QetDiagnosticJson`。汇总诊断会从诊断 payload 中提取 `runtime_version`,优先采用 `RoutingConnectionBatch` 的版本号,没有批量布线结果时再回退到预检或路径网络版本,便于确认当前工程是否加载了最新运行模块。该汇总不重新生成路径、不重新布线,也不访问 QET 或修改数据库;它只是把 FreeCAD 已保存的诊断状态集中显示并固化到诊断树中,便于把一次装配/布线测试的问题快速分成“准备不足、路径网络问题、批量布线问题”三类。 + +当最新 `RoutingConnectionBatch` 存在时,汇总诊断会把它视为最终诊断入口。若批量报告中已经内嵌 `routing_path_network_diagnostic`,则不再要求额外存在独立 `RoutingPathNetwork` 诊断对象;同样,也不会因为用户没有单独执行 `检查布线准备度` 而把一次完整批量布线误判为失败。这样面板既支持完整流程,也支持现场更常用的简化流程:直接 `生成布线连接` 后点击 `汇总布线诊断`。 + +如果汇总时发现旧版诊断对象存在但 `QetDiagnosticJson` 为空,会追加 `diagnostic_json_empty / 诊断 JSON 为空`。这类对象不能证明当前布线状态有效,应重新运行对应诊断或重新生成布线连接,让新版本写入完整 `QetDiagnosticJson`。 + +汇总诊断还会扫描 `QETWiring_04_Routed` 下的已生成导线。如果发现旧版 `RoutedConnection` 缺少单线 `QetRouteDiagnosticsJson`,会追加 `routed_wire_diagnostics_missing / 导线诊断缺失`,并给出一条导线 Label 示例。这能区分“模型里看得到线”和“这条线具备当前版本碰撞、柜内边界、路径质量等诊断数据”。 + +旧版批量诊断对象可能没有 `issue_codes`,但会保留 `route_status_counts`、`skipped_missing_terminal` 和 `missing_endpoint_samples`。汇总诊断会从这些旧字段反推出 `routing_errors`、`missing_terminals` 和 `missing_endpoints`,并在中文摘要里显示“结果状态:错误 N 条,缺失端子 N 条”。这样用户重新打开旧工程时,不会因为旧诊断缺少新版字段而误以为没有布线错误。 + +如果单线 `QetRouteDiagnosticsJson` 存在但无法解析为合法 JSON,会追加 `routed_wire_diagnostics_invalid / 导线诊断 JSON 无效`。这类对象同样不能作为当前版本诊断依据,需要重新生成布线连接,让 FreeCAD 重新写入完整单线诊断。 当导线因为缺少布线路径网络被跳过时,批量报告会显示一条“缺路径网络示例”,包含导线号、起终点端子标签和已记录的失败原因。这里既包括整份文档没有有效路径段,也包括路径网络存在但该导线两端无法连通、端子接入距离阈值过小等情况。手动测试时可先按该示例定位设备两端附近是否缺线槽、`UserPath`、过线孔或布线面路径,或判断是否需要调整端子接入距离。 -当最终导线虽然布通、但起点或终点到主路径入口距离超过警戒阈值时,批量报告会显示“接入距离提示”,列出触发导线数量、一条导线样例及起点/终点接入距离。这个提示不阻止生成导线,用于暴露设备附近缺少局部路径、主路径离端子过远或端子接入距离设置过大的情况。批量诊断 JSON 也会记录 `route_entry_distance_warning_count` 和 `route_entry_distance_warning_samples`,便于导出后定位全部样例。 +批量 `issue_codes` 会把缺端子原因从样例提升到顶层:`missing_device_binding_metadata / 端点缺少绑定信息`、`device_not_in_3d_scene / 3D场景缺少设备`、`no_3d_terminals_for_element / 设备缺少工程端子`、`no_3d_terminals_for_instance / 实例缺少工程端子`、`terminal_uuid_not_in_element / 端子UUID不匹配`。这样真实工程中只有少量缺端子时,也可以不展开 JSON 就判断下一步是补 QET 端点元数据、补 3D 设备装配/绑定,还是核对同设备端子 UUID。 + +批量布线原始 report 和 compact 批量诊断都会同步写入 `missing_terminal_summary`,复用汇总诊断的缺端子分组口径。`reason_code_counts` 统计每类原因,`device_groups[]` 按缺失侧设备聚合 `element_uuid`、`instance_id`、缺失端子、端子 UUID 和相关导线,便于把“缺 3D 设备”或“端子 UUID 不匹配”的问题直接交给装配/绑定流程处理。脚本或面板二次处理时应优先读取这个结构化字段,而不是解析中文报告里的 `需补端子设备` 文本。 + +当用户从已打开的 FCStd 任务对象直接执行布线,而任务对象自身没有携带完整 `devices[]` 时,FreeCAD 会尝试从当前 QET 交换上下文的 `2d_to_3d.json` 只读回补设备列表。该回补只合并 `devices[]`,不会用磁盘 JSON 覆盖当前 FreeCAD 文档中的导线任务;项目 UUID 不一致时会拒绝回补。批量 report 会写入 `context_devices_loaded`、`context_device_count` 和 `context_devices_json_path`,用于确认本次是否加载了上下文设备列表。这样真实工程中 `UD:8 / UD:10 / UD:5` 这类缺设备分组可以继续带出 `instance_id`,方便装配/绑定侧定位。 + +当批量布线已经生成导线,但 `route_path_usage.main_path_routes = 0` 且 `fallback_routes > 0` 时,诊断会追加 `main_path_not_used / 未使用线槽或用户主路径`。这表示导线虽然能连通,但全部依赖 `RoutingRange` 或 `AuxiliaryPath` 兜底路径,没有真正进入线槽、过线孔或用户主路径网络。手动测试看到该提示时,应优先补线槽中心路径、柜内黄色 `UserPath`、设备局部出线路径或主路径桥接,再重新生成布线连接。 + +批量诊断还会记录 `route_network_carrier_kind_counts` 和 `route_network_main_path_carriers`。如果 `route_network_main_path_carriers > 0` 但仍触发 `main_path_not_used`,说明 FreeCAD 已经识别到线槽/UserPath/过线孔主路径,但这些主路径没有接入端子局部网络,或距离/连通关系导致最终选路仍退回布线面。此时优先检查线槽端点、线槽到端子附近的桥接、`TerminalAccess` 最大距离、`UserPath` 是否贴近线槽中段,以及是否需要点击“按诊断建议生成桥接”。 + +当最终导线虽然布通、但起点或终点到主路径入口距离超过警戒阈值时,批量报告会显示“接入距离提示”,列出触发导线数量、一条导线样例、起点/终点接入距离和该样例实际经过的路径标签。这个提示不阻止生成导线,用于暴露设备附近缺少局部路径、主路径离端子过远或端子接入距离设置过大的情况。批量诊断 JSON 也会记录 `route_entry_distance_warning_count` 和 `route_entry_distance_warning_samples`,其中 warning sample 会保留 `route_source_labels`,便于导出后定位全部样例。 -当单条路线使用 `RoutingRange` 或 `AuxiliaryPath` 时,批量报告会提示“路径质量提示”,说明该导线可能没有完全优先进入线槽。这个提示不阻止布线,只用于暴露“当前路径依赖布线面兜底”的情况,方便后续补线槽、补 `UserPath` 或调整设备位置。批量诊断 JSON 也会记录这类提示:`route_quality_warning_count` 表示依赖布线面/辅助路径的导线数量,`route_quality_warning_samples` 保留少量导线样例及其使用的 carrier 类型。 +单根导线对象也会展开端子接入距离,避免手动测试时只能打开 JSON。`QetRouteEntryDistanceMm` / `QetRouteExitDistanceMm` 分别表示起点、终点端子出线点到主路径网络入口的距离;`QetRouteEntryPointMode` / `QetRouteExitPointMode` 表示接入点来自路径端点还是中段投影;`QetRouteEntryCandidateRank` / `QetRouteExitCandidateRank` 表示最终采用的入口候选排名;`QetRouteAccessWarningDistanceMm` 保存本次告警阈值。若 `QetRouteAccessStatus=LongAccessWarning`,说明该线虽然已经布通,但起点或终点接入主路径过长,`QetRouteAccessWarningSides` 会标出 `entry`、`exit` 或二者都触发。完整明细同步写入 `QetRouteDiagnosticsJson.access`。手动测试看到该告警时,应优先补设备局部路径、把用户路径/线槽靠近设备端子,或重新检查设备是否已经装配到正确位置。 + +当候选路线评分发现最终候选仍有接入段或候选折线穿过障碍包围盒时,批量报告会显示“接入避障提示”,列出触发导线数量、一条样例和该样例实际经过的路径标签。这个提示通常表示当前附近缺少可绕开的线槽、`UserPath` 或设备局部路径;它不替代最终碰撞状态,但能帮助区分“已经布通但入口路径仍不理想”的情况。批量诊断 JSON 也会记录 `route_candidate_obstacle_warning_count` 和 `route_candidate_obstacle_warning_samples`,其中 warning sample 会保留 `route_source_labels`;`route_samples[].network` 中仍保留样例路线自身的候选入口 rank、候选评分和候选障碍命中数。 + +入口候选会按“投影点 + carrier”去重,避免同一个近路径因为桥接边或重复边占满候选名额。有障碍物参与候选评分时,系统除保留距离排序靠前的候选外,还会额外保留一批接入折线不穿过障碍包围盒的候选,再统一评分。这样可以减少“近入口需要穿设备、远一点入口更干净,但远入口被候选上限截掉”的穿模问题。 + +最终导线碰撞诊断会区分两类碰撞:`HardIntersection` 表示导线段穿过原始障碍包围盒,中文报告显示为“硬碰撞”;`ClearanceWarning` 表示导线没有穿过原始障碍,但进入了按安全间隙膨胀后的包围盒,中文报告显示为“安全间隙”。批量报告会增加“碰撞分类”,compact 诊断 JSON 会记录 `collision_kind_counts`,用于快速判断当前问题是明显穿模还是安全距离不足。单根导线对象的 `QetRouteDiagnosticsJson.collisions[]` 和批量碰撞样例都会尽量保留该导线实际经过的路径标签,例如 `路径 主线槽A`,方便判断是线槽中心线、`UserPath` 还是兜底路径导致穿模。 + +批量 `issue_codes` 会在 `collision_warnings` 之外进一步追加碰撞处理分类:`structural_collision_candidates / 结构件碰撞候选` 表示存在可确认的柜体、门板、支架等结构件碰撞候选;`device_or_layout_collisions / 设备/布局碰撞` 表示存在真实设备或布局碰撞。这样汇总诊断和手动测试可以直接区分“可确认忽略的结构件”与“需要补路径或调整装配的设备碰撞”。 + +如果端子或设备模板明确提供了 `QetTerminalLocalRoutePointsJson` 局部出线路径,最终导线会保留这些局部路径点,并在 `QetRouteDiagnosticsJson.endpoint_access` 中写入起点/终点接入路径。碰撞诊断会把这些明确的端子局部接入段视为设备内部或设备附近的受控出线段,不把它们当成主路径穿模问题;但局部路径接入后的柜内主路径、中段线槽和 `UserPath` 仍会继续做碰撞诊断。这样可以减少设备壳体附近的误报,同时不会隐藏真正穿过柜体、设备或主路径障碍的导线。 + +批量 `collision_samples[]` 也会保留 `wire_object_label`,其值与树目录导线对象 Label 一致。这样当报告显示某条线有硬碰撞或安全间隙告警时,可以直接用 `wire_object_label` 在 `QETWiring_04_Routed` 下找到对应导线,再检查 `QetRouteCollisionStatus`、路径来源和碰撞包围盒。 + +单根导线对象会同步展开碰撞状态:`QetRouteCollisionCount` 表示总碰撞/间隙告警数量;`QetRouteHardIntersectionCount` 表示硬碰撞数量;`QetRouteClearanceWarningCount` 表示安全间隙告警数量;`QetRouteCollisionStatus` 会在 `NoCollision`、`ClearanceWarning`、`HardIntersectionWarning` 之间切换。手动测试时,如果状态是 `HardIntersectionWarning`,应优先检查导线是否穿过设备、线槽壁或柜体;如果只是 `ClearanceWarning`,通常表示路线贴近障碍,需要调大线槽/用户路径距离、调整设备位置或降低安全间隙阈值后复测。 + +`3D 布线连接` 面板提供“障碍安全间隙 mm”设置,对应 `obstacle_clearance`。该值用于膨胀障碍包围盒:值越大,越容易把贴近设备或柜体的导线标记为 `ClearanceWarning`,也会让候选入口评分更倾向避开贴近障碍的接入折线;值为 0 时只按原始障碍包围盒判断明显穿模。 + +柜内区域边界可以由 FreeCAD 文档中的对象提供,给对象设置 `QetRoutingBoundaryKind = "CabinetInterior"` 即可把该对象包围盒视为柜内可布线范围。边界对象不写数据库,也不会被当作障碍物;它先参与路径图过滤,再参与最终导线候选评分。当存在 `CabinetInterior` 时,系统会优先构建“柜内路径图”,只保留完全落在柜内边界内的路径段并先在这张图上求路;只有柜内路径图不可达时,才回退到原始路径图并保留柜内越界告警。这样即使柜外路径几何距离更近、柜内路径稍远,也会优先选择仍在柜内的线槽、`UserPath` 或支撑面路径。批量诊断 JSON 的 `route_samples[].network` 会记录 `boundary_aware`、`boundary_filtered`、`boundary_filtered_segments` 和 `route_candidate_boundary_violations`,用于确认本次布线是否启用了柜内边界约束、是否先使用了柜内过滤图,以及最终候选是否仍存在越界点。 + +如果柜内过滤图无法连通两端,系统仍会回退到原始路径图并生成综合评分最高的路径,但批量报告会显示“柜内边界提示”,指出有多少条导线最终路径仍越出柜内区域,并给出一条导线样例、越界点数量和该样例实际经过的路径标签。compact 诊断 JSON 同步记录 `route_candidate_boundary_warning_count` 和 `route_candidate_boundary_warning_samples`,其中 warning sample 会保留 `route_source_labels`。单根导线对象也会写入 `QetRouteBoundaryAware=true`、`QetRouteBoundaryStatus=BoundaryWarning` 和 `QetRouteBoundaryViolationCount`,便于选中某条导线后直接判断它是否跑出柜内区域。手动测试看到该提示时,应优先补柜内 `UserPath`、线槽或设备局部路径;如果边界对象本身建模过窄,也可以调整 `CabinetInterior` 对象包围盒。 + +有柜内边界时,入口候选不会只按几何距离截断。系统会先保留距离排序靠前的候选,再额外保留一批投影点位于 `CabinetInterior` 内部的候选,最后统一按路径成本、接入距离、障碍命中和柜内越界点数评分。这样可以避免柜外近路径数量较多时,把稍远但正确的柜内 `UserPath` / 线槽在评分前就挤掉。 + +路径网络检查也会提前检查主路径 carrier 自身是否越出柜内边界。当文档中存在 `CabinetInterior` 时,`WireDuct`、`UserPath`、`WiringCutOut` 等非 `TerminalAccess` 路径点如果落在边界外,会记录 `route_carriers_outside_boundary / 路径越出柜内边界`,并在中文报告中给出路径标签和越界点数量。这用于在生成导线前发现“用户路径或线槽中心线本身画到柜外”的问题,避免后续所有导线都沿错误主路径求路。 + +路径网络检查还会检查工程端子是否落在柜内边界内。当端子原点或端子出线末端位于 `CabinetInterior` 外时,会记录 `terminals_outside_boundary / 端子越出柜内边界`,并高亮对应端子对象。这主要用于发现设备还停留在导入位置、端子 LCS 没有跟随装配实例移动、或柜内边界对象标得过窄等装配态问题。 + +`3D 布线连接` 面板摘要会显示当前文档内已识别的柜内边界数量,例如 `柜内边界:1`。执行“选中对象作为柜内边界”后,可先看摘要确认边界对象已经生效,再生成布线路径网络和布线连接。 + +被标记为 `CabinetInterior` 的对象只作为边界使用,不会再被自动识别成线槽、安装面、过线孔或 `UserPath` 源对象。这样可以避免辅助边界盒、柜体内腔对象或用户误选的柜体对象在生成布线路径网络时又变成导线可走 carrier。 + +路径约束已支持 SW/EPLAN 风格“禁止经过”和“必须经过”的第一版能力。调用自动布线时可以通过 options 传入 `forbidden_route_carrier_names`、`forbidden_route_carrier_labels`、`forbidden_route_carrier_source_names`、`forbidden_route_carrier_source_labels` 或 `forbidden_route_carrier_kinds`,Dijkstra 搜索会直接跳过匹配 carrier 的边;也可以传入 `required_route_carrier_names`、`required_route_carrier_labels`、`required_route_carrier_source_names`、`required_route_carrier_source_labels` 或 `required_route_carrier_kinds`,Dijkstra 状态会记录已经经过的必经 carrier 或源对象,只有所有必经条件满足时才允许结束。批量 `wires[]` 中的单条导线任务也可以携带这些同名字段,用于表达“某一根导线必须/禁止经过某些路径”。该能力当前先作为算法层和导线任务可选字段,后续可接入面板、FreeCAD carrier 属性或 QET 导线规则;如果禁止/必经规则导致两端不再连通,导线会按“缺少布线路径网络/不连通”进入失败诊断。 + +当必经/禁经规则导致无可用路径时,单条布线会提示“没有满足路径约束的布线路径网络”;批量布线会把它归入 `MissingRouteNetwork`,并在“缺路径网络示例”的 `error` 字段里保留路径约束原因。这样手动测试时可以区分“真的没有线槽/UserPath”和“路径存在但被规则禁止或必经规则无法满足”。 + +当路径约束成功生效并生成导线时,`QetRouteNetworkJson` 会保留 `route_constraints`,记录本路线使用到的 required / forbidden carrier 名称、标签、源标签或类型。批量 compact 诊断的 `route_samples[].network.route_constraints` 也会保留这些字段,方便回看某条线为什么没有走最近路径。 + +批量生成布线连接的中文报告会汇总路径约束使用情况:当有导线应用必经/禁经规则时,报告显示“路径约束提示”,列出触发导线数量和一条样例导线,并按“必须经过/禁止经过”展示标签、名称、源标签或类型。compact 诊断 JSON 也会记录 `route_constraint_warning_count` 和 `route_constraint_warning_samples`,便于导出后快速确认哪些导线受全局或单线规则影响。 + +为了便于 FreeCAD 手动测试,也可以直接在路径 carrier 对象上设置 `QetRouteConstraintMode`。值为 `Forbidden` 时,该 carrier 会被所有自动布线跳过;值为 `Required` 时,所有自动布线都必须经过该 carrier,否则进入路径约束失败诊断。这个对象属性适合验证工程规则或临时屏蔽某段线槽;如果未来要做到“只对某一根导线生效”,仍建议通过单条 `wires[]` 的 required/forbidden 字段表达。 + +`3D 布线连接` 面板提供“选中路径必须经过”“选中路径禁止经过”“清除选中路径约束”和“清除全部路径约束”四个入口。用户可以选中已经生成的 route carrier,或选中草图、Draft 线等源路径对象,然后点击对应按钮写入或清空 `QetRouteConstraintMode`。如果源路径对象还没有生成 carrier,面板会按“源路径”计数;后续生成 `UserPath` 时会继承该约束,避免用户必须严格按“先生成路径、再设置约束”的顺序操作。如果多次手动测试后不确定哪些路径仍保留 Required / Forbidden,可使用“清除全部路径约束”,它会同时清空当前文档内 route carrier 和源路径对象上的约束,避免重新生成路径网络后旧约束再次继承回来。该设置是 FreeCAD 文档内的全局路径规则,会影响后续所有自动布线。 + +手动编辑属性时,`QetRouteConstraintMode` 同时支持英文和中文别名:`Required`、`必须经过`、`必经` 都表示必经路径;`Forbidden`、`禁止经过`、`禁经`、`禁止` 都表示禁经路径。面板按钮仍写入标准英文值,便于程序稳定判断。 + +面板摘要会显示当前文档中 carrier 级路径约束数量,例如 `路径约束:必经 1,禁经 1`。如果约束写在尚未生成 carrier 的草图、Draft 线等源路径对象上,或写在线槽、过线孔、支撑面等已标记路由源对象上,摘要会单独显示 `源路径约束:必经 1,禁经 0`。前者代表已经参与当前路径网络求路的 carrier 规则,后者代表后续生成或刷新 `UserPath` / `WireDuct` / `WiringCutOut` 等 carrier 时会继承的源对象规则。标记或清除路径约束后,可先看摘要确认状态,再重新生成布线连接。 + +当用户选中源路径对象(例如草图、Draft 线或线槽源对象)设置路径约束时,系统会同时把 `QetRouteConstraintMode` 写到源对象和它已经生成的 carrier 上。后续即使清除走线路径并重新生成 carrier,新 carrier 也会继承源对象上的 Required / Forbidden 约束,避免工程规则在刷新路径网络后丢失。如果源路径对象的约束已经清空,刷新 `UserPath` 时也会同步清空旧 carrier 上残留的 Required / Forbidden,避免多轮手动测试后旧规则继续影响自动布线。 + +同一个草图或 Draft 源对象可能包含多条不连通 `Wire`,生成时会拆成多条 `UserPath`。当约束来自源对象时,系统会按 `QetRouteSourceName` 聚合判断:`Required` 表示路线至少经过该源对象生成网络中的一条相关路径即可满足,不再强制同时经过所有生成子路径;`Forbidden` 表示该源对象生成的全部路径都不可走。这样更符合甲方“预先画黄色路径作为布线输入”的操作习惯,也避免多分支草图被误判为必经规则无法满足。 + +当单条路线使用 `RoutingRange` 或 `AuxiliaryPath` 时,批量报告会提示“路径质量提示”,说明该导线可能没有完全优先进入线槽。这个提示不阻止布线,只用于暴露“当前路径依赖布线面兜底”的情况,方便后续补线槽、补 `UserPath` 或调整设备位置。报告会尽量显示具体的布线面/辅助路径 carrier 标签,例如 `示例 N4111 使用布线面:安装板辅助路径`;没有具体标签时仍回退显示 carrier 类型。批量诊断 JSON 也会记录这类提示:`route_quality_warning_count` 表示依赖布线面/辅助路径的导线数量,`route_quality_warning_samples` 保留少量导线样例、使用的 carrier 类型和 `route_carrier_labels`。 + +单根导线对象也会展开路径质量状态:`QetRouteQualityStatus=NormalPath` 表示该线没有使用布线面/辅助路径兜底;`QetRouteQualityStatus=FallbackPathWarning` 表示实际路线经过了 `RoutingRange` 或 `AuxiliaryPath`。`QetRouteFallbackCarrierKinds` 会列出兜底 carrier 类型,`QetRouteFallbackCarrierLabels` 会列出具体标签,例如安装板辅助路径。这个状态不代表布线失败,但说明第一版算法是在“能连通”的基础上用了低优先级路径;手动测试看到它时,应优先补线槽、黄色草图 `UserPath`、过线孔或设备局部路径,让导线进入更明确的工程主路径。 + +当并行 lane 数超过实际经过路径的最小容量时,批量报告会提示“容量提示”,显示最大并行线数、路径最小容量,并给出一条样例导线和真实经过的路径名称。这个提示不阻止布线;它用于暴露线槽外共线拥挤、线槽容量设置过小或需要增加并行路径的场景。compact 诊断 JSON 同步记录 `route_capacity_pressure_warning_count` 和 `route_capacity_pressure_warning_samples`,样例包含导线、并行线数、最小容量、lane index、carrier 名称和源路径标签。若容量压力来自同一个草图拆出的多条 `UserPath`,中文报告和 compact 诊断会优先显示 `源路径标签(路径1)` 这类可回溯到黄色草图线的标签,而不是只显示自动生成的 carrier 名称。 + +`3D 布线连接` 面板提供“共路复用惩罚”设置,对应 `segment_reuse_penalty`。当某段路径的已用导线数超过该 carrier 的 `QetRouteCarrierCapacity` 时,Dijkstra 会按该惩罚增加复用成本;调高后更倾向绕到备用线槽或 `UserPath`,调低后更倾向继续走几何距离更短的公共路径。该参数只影响搜索成本,不代表真实线槽填充率校核。 路径网络检查还会识别“只有 `RoutingRange`、没有 `WireDuct` / `UserPath` / `WiringCutOut` 主路径”的情况,并记录 `routing_range_only_network`。这类网络可以作为无线槽或路径不完整时的临时兜底,但不是推荐的第一版主路径形态;手动测试看到该提示时,优先补线槽、补 `UserPath` 或补过线孔路径。 @@ -569,6 +747,8 @@ QetWiringCutOutBridgeExtensionMm = 20.0 当单条路线的最大并行线数超过该路线 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。 +单根导线对象会展开共路和容量状态,便于选中导线后直接检查。`QetRouteLaneIndex` 表示该线在共享路径中的 lane 序号,`QetRouteLaneAxis` / `QetRouteLaneOffsetMm` 表示显示错位方向和偏移量,`QetRouteParallelWireCount` 表示到该 lane 为止的并行线数量,`QetRouteMinCarrierCapacity` 表示该线实际经过路径的最小 carrier 容量。若 `QetRouteCapacityStatus=CapacityWarning`,说明该线所在共享路径的并行线数已经超过当前路径容量,应补备用线槽、用户路径或调高真实 carrier 容量属性。 + ### 5.3 布线连接功能 已完成: @@ -586,6 +766,7 @@ QetWiringCutOutBridgeExtensionMm = 20.0 11. 自动导线可见显示并保存到 FreeCAD 文档。 12. 生成布线连接时保存 `QetRouteTrackJson`,记录实际经过的 `WireDuct` / `RoutingRange` / `TerminalAccess` / `WiringCutOut` carrier。 13. 支持检查布线路径网络,诊断孤立网络、未接入端子和疑似线槽端点断点,并写入 `QETWiring_05_Diagnostics`。 +14. 支持柜内边界约束:当文档中存在 `QetRoutingBoundaryKind = "CabinetInterior"` 对象时,自动布线会先在柜内过滤后的路径图上求路,再回退原始路径图;路径网络检查也会提前提示主路径 carrier 或工程端子越出柜内边界。 ### 5.4 FreeCAD 面板 @@ -599,6 +780,11 @@ QET模板 -> 3D布线连接 ```text 准备布线布局空间 +选中对象作为柜内边界 +选中路径必须经过 +选中路径禁止经过 +清除选中路径约束 +清除全部路径约束 生成布线路径网络 检查布线路径网络 生成布线连接 @@ -639,7 +825,7 @@ tests/python/freecad_exchange_auto_routing_test.py 20. “生成布线连接”会先更新同一套布线路径网络,再按全部 QET 导线任务批量求路。 21. 相邻主路径端点在容差内会被网络自动连通;支路端点靠近主路径中段时也会投影桥接;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。 22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。 -23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象。 +23. 导线会保存 routing track;网络检查会生成 `RoutingPathNetwork` 诊断对象,并在返回结果中同步给出 `issue_codes`。诊断对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示是否通过;`QetDiagnosticIssueCodes` 直接列出问题码;`QetDiagnosticIssueLabels` 直接列出中文问题标签;`QetDiagnosticMessage` 保存中文摘要,`QetDiagnosticJson` 保存路径网络诊断明细和 `issue_codes`。 24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。 25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络,并支持通过 `QetWiringCutOutBridgeExtensionMm` 按对象调整外扩距离。 26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。 @@ -654,19 +840,99 @@ tests/python/freecad_exchange_auto_routing_test.py 35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。 36. 批量布线报告会在最大并行线数超过路径最小容量时显示容量提示,但当前仍不做真实填充率计算。 37. `3D 布线连接` 面板提供“并行线间距 mm”、“并行线最大偏移 mm”和“并行线方向”设置,用于调整多线共路时的可视 lane 偏移。 -38. 最终导线选路会在多个入口候选中避开接入段穿障碍的入口,并优先选择可避障的线槽 / `UserPath` 入口。 -39. 同一入口下的端子接入正交折线会尝试不同轴向顺序,优先选择不穿过障碍包围盒的折线。 +38. `3D 布线连接` 面板提供“障碍安全间隙 mm”设置,用于调整安全间隙告警和接入候选避障评分。 +39. `3D 布线连接` 面板提供“共路复用惩罚”设置,用于调整超过 carrier 容量后的复用成本和绕行倾向。 +40. 最终导线选路会在多个入口候选中避开接入段穿障碍的入口,并优先选择可避障的线槽 / `UserPath` 入口。 +41. 入口候选按投影点和 carrier 去重;存在障碍时会额外保留干净接入候选,避免重复近候选挤掉稍远的避障入口。 +41. 同一入口下的端子接入正交折线会尝试不同轴向顺序,优先选择不穿过障碍包围盒的折线。 40. 并行导线可视 lane 偏移默认限制在固定上限内,防止密集共路时导线被显示到柜外。 41. 完整自动布线流程会使用支路端点到主路径中段的投影桥接,避免这类支路网络被误判为孤立。 42. `QetRouteTrackJson` 会标记实际走过的自动桥接段,并记录本路线实际使用的桥接段数量。 43. 批量布线报告的路径示例会跳过虚拟桥接段,只列出真实经过的源对象标签。 44. 共路 lane 计数和路径复用惩罚会跳过虚拟桥接段,避免仅共享自动桥接边的导线被误判为真实共路。 +45. `3D 布线连接` 面板提供“选中对象作为柜内边界”,用于把选中对象的包围盒标记为 `CabinetInterior`,让自动布线优先留在柜内区域。 +46. 面板摘要会显示已识别的柜内边界数量,方便确认边界约束是否生效。 +47. `CabinetInterior` 边界对象不会被自动识别成 `WireDuct`、`RoutingRange`、`WiringCutOut` 或 `UserPath` 路径源。 +48. 柜内边界存在时,入口候选会额外保留柜内候选参与评分,并且系统会先尝试柜内过滤路径图,避免大量柜外近路径或柜外捷径挤掉稍远但正确的柜内路径。 45. 路径质量提示会按非桥接段重新判断 carrier 类型,避免把虚拟桥接到 `RoutingRange` 误报为真实使用布线面兜底。 46. 缺少布线路径网络或路径网络两端不连通时,批量布线报告会显示一条导线、端点样例和失败原因,便于直接定位需要补路径的设备区域。 47. “端子接入最大距离”同时约束自动 `TerminalAccess` 和最终导线入口候选,防止最终求路绕过面板设置生成超长接入线。 48. 批量诊断 JSON 的 route sample 会跳过虚拟桥接段统计 carrier 类型和 carrier 名称,保持与中文报告一致。 49. 最终导线路由不会把 `TerminalAccess` 当作公共 transit carrier,入口候选也会优先真实主路径,避免端子局部接入线被误用来桥接主路径缺口或作为其它导线的起步路径。 50. 批量布线报告会提示最终导线起点/终点接入距离过长的样例,用于排查设备附近缺局部路径或主路径离端子太远。 +51. 单根导线对象会展开 `QetRouteEntryDistanceMm`、`QetRouteExitDistanceMm` 和 `QetRouteAccessStatus`,用于选中某条线后直接判断是否存在长接入。 +52. 单根导线对象会展开 `QetRouteSourceLabels` 和 `QetRouteCarrierNames`,用于直接查看该线实际经过哪些线槽、黄色草图路径或用户路径。 +53. 单根导线对象会展开 `QetRouteCollisionStatus`、`QetRouteHardIntersectionCount` 和 `QetRouteClearanceWarningCount`,用于直接区分穿模和安全间隙告警。 +54. 单根导线对象会展开 `QetRouteQualityStatus`、`QetRouteFallbackCarrierKinds` 和 `QetRouteFallbackCarrierLabels`,用于判断该线是否依赖布线面/辅助路径兜底。 +55. 批量诊断的 `route_samples[]` 会同步输出 `access`、`collision_summary`、`quality`、`capacity`、`boundary` 状态分组,便于不逐根选中导线也能复盘样例路线。 +56. 生成导线对象的树目录 Label 会包含导线标识、起终点端子和状态,便于在 FreeCAD 左侧树中定位问题导线。 +57. 单根导线对象会展开 `QetRouteIssueCodes` 和 `QetRouteIssueLabels`,用与批量诊断一致的问题码汇总该线自身的问题。 +58. 批量诊断的 `route_samples[]` 会优先保留带问题码的路线样例,避免问题线排在后面时被 sample limit 截掉。 +59. 批量诊断的 `route_samples[]` 会保留 `wire_object_label`,用于从诊断 JSON 直接定位 FreeCAD 树目录中的导线对象。 +60. 批量碰撞样例 `collision_samples[]` 会保留 `wire_object_label`,便于从穿模/安全间隙告警直接定位问题导线。 +61. 接入距离、路径质量、候选入口、柜内边界、路径约束和容量压力 warning samples 会保留 `wire_object_label`,统一诊断定位方式。 +62. 路径网络长接入样例会保留父设备、端子全局点、接入折点、主要超长方向和各轴长度。真实工程中如果 `terminal_access_dominant_axis=z` 且竖向长度占大头,可以直接判断为端子/设备高度或局部出线路径问题,而不是主路径网络断开。 +63. 碰撞样例会保留障碍对象的父装配名称和标签,方便把 `NAUOxxx` 这类导入零件追溯到前门、柜体、安装板或具体设备,再决定是标记忽略碰撞还是补柜内路径。 +64. 面板提供“选择越界路径/端子”,从最新 `RoutingPathNetwork` 诊断的 `route_carriers_outside_boundary` 和 `terminals_outside_boundary` 反向选择越出柜内边界的路径 carrier 与工程端子。该功能只定位对象,不自动调整边界、不移动设备、不写数据库。 +64. 批量布线报告的 `top_collision_obstacles[]` 不再只记录对象标签和次数,还会记录对象名称、父装配、硬碰撞/安全间隙分类计数;中文摘要也会显示父装配,例如 `NAUO118(CABINET ASS'Y) 18 处`,便于区分柜体/门板 AABB 误报和真实设备穿模。 +65. 缺失端点样例会记录同 2D 设备、同 3D 实例下的 FreeCAD 工程端子数量,并输出原因码。真实工程里若原因是 `missing_device_binding_metadata`,说明 QET 导线任务端点缺少 `element_uuid`,FreeCAD 无法判断缺失端子属于哪个 2D 设备;第一版不要求 QET 在 `wires[]` 端点提供 `start/end_instance_id`。若原因是 `device_not_in_3d_scene`,说明该 2D 设备当前没有对应 3D 设备实例,应回到设备导入、装配和 2D/3D 绑定流程排查;若原因是 `no_3d_terminals_for_element`,说明设备实例在场景中但没有生成工程端子,应回到端子生成流程排查。这些都不是路径网络问题。 +66. 高发碰撞对象会输出处理建议。疑似柜体、门板、支架、盖板等结构件时,建议用户确认后通过现有“选中对象忽略碰撞”标记为 `PassThrough`;疑似设备或安装区域碰撞时,建议补主路径、局部出线路径或调整装配,而不是直接忽略。 +67. 面板提供“选择高发碰撞对象”,从最新批量诊断 `top_collision_obstacles[]` 反向选择 FreeCAD 对象,方便现场确认后再手动标记忽略或调整路径。该功能只定位对象,不自动修改碰撞规则。 +68. 面板提供“选择碰撞导线”,从最新批量诊断 `collision_samples[]` 和带 `collision_warnings` 的 `route_samples[]` 中反向选择 RoutedConnection 导线对象,便于和高发碰撞对象一起核对穿模位置。该功能只定位导线,不重新求路。 +68. 面板提供“选择缺主路径导线”,从最新 `route_samples[]` 和导线对象自身的 `QetRouteIssueCodes` 中选择带 `main_path_detour_missing` 的 RoutedConnection 导线。该功能用于定位“选择性避障重算本可减少碰撞,但会退回到辅助路径/布线面兜底,因此被当前主路径优先策略拒绝”的导线;下一步应补 `UserPath`、桥接主路径、调整线槽入口或完善设备局部出线路径,不自动接受 fallback 结果。 +68. 自动布线会对明确的 `main_path_detour_missing` 做一次收敛处理:当选择性避障已经得到碰撞更少的 fallback 折线,但该折线因包含 `RoutingRange` 被拒绝时,系统会把这条折线固化为 `MainPathDetourPath` 类型的 `UserPath`,再按 `兜底区域 -> 当前主路径` 生成 `MainPathDetourBridge`,随后只重试受影响导线。这样保持主路径优先,不直接接受宽泛布线面兜底,同时避免整批导线二次全量重跑。 +69. 面板提供“选择长接入端子”,从最新批量诊断 `routing_path_network_diagnostic.long_terminal_accesses[]` 中反向选择端子对象,便于检查端子高度、设备装配和局部出线路径。该功能只定位端子,不修改端子或路径数据。 +70. 面板提供“选择缺端子设备”,从最新批量诊断 `missing_endpoint_samples[]` 的缺失侧读取 `*_instance_id` / `*_element_uuid` 并反向选择 3D 设备,便于补工程端子或检查 2D/3D 绑定。若缺失设备不在当前场景中,控制器仍会返回 `missing_terminal_device_instance_ids[]`、`missing_terminal_device_element_uuids[]` 和可读标签,状态栏也会显示 instance_id,便于把缺设备清单交给装配/绑定流程。该功能只定位设备,不自动创建端子、不修改 QET 数据。 +71. 面板提供“选择缺端子另一端”,从缺端子样例中选择已找到的另一端工程端子,便于确认失败导线本来要连接到哪里,再对照缺失侧设备和端子脚号。该功能只定位端子,不自动补端子、不写数据库。 +72. 面板提供“选择缺端子候选端子”,从 `*_instance_terminal_samples` / `*_element_terminal_samples` 中反向选择同设备或同实例已有工程端子,便于排查 `terminal_uuid_not_in_element` 这类“同设备已有端子但 UUID 不匹配”的问题。该功能只定位候选端子,不自动改绑定、不写数据库。 +73. 碰撞障碍语义按 FreeCAD 父装配链递归识别。用户把柜体、门板、支架、盖板等父装配标记为 `PassThrough` 后,深层导入子零件也会被排除在导线障碍之外;碰撞样例的 `parent_refs` 也会尽量输出完整父链,避免只看到中间 `Compound/NAUO`。 +73. 第一版会自动过滤一类未绑定导入结构件碰撞:障碍物没有 QET `element_uuid`,并且位于 `QET Exchange Devices / QETCabinet / LinkGroup / Compound / NAUO` 等导入装配上下文,同时名称或父链命中柜体、门板、支架、盖板等结构关键词时,不再计入导线碰撞。该规则不作用于带 `element_uuid` 的 3D 设备,因此不会吞掉真实设备/端子/断路器碰撞。 +74. 面板提供“选择碰撞父装配”,从最新 `top_collision_obstacles[]` 的 `parent_names / parent_labels` 反向选择父装配,适合先确认前门、柜体、支架等总成是否可穿越,再统一标记 `PassThrough`。该功能只定位父装配,不自动忽略碰撞。 +75. 面板提供“确认结构件忽略碰撞”,按最新诊断只把疑似柜体、门板、支架、盖板等结构件碰撞的最近结构父装配标记为 `PassThrough`。该动作不会沿父链继续标记 `QET Exchange Devices` 这类工程根组,不会标记 `review_device_or_layout_collision`,也不写数据库;重新生成布线连接后,下级导入结构子件会因对应父装配 `PassThrough` 被排除出障碍候选,真实设备碰撞仍会继续保留诊断。 +76. 面板提供“选择设备碰撞对象”,只从最新 `top_collision_obstacles[]` 中选择 `review_device_or_layout_collision` 候选。该功能用于处理结构件忽略后仍剩余的真实设备/布局碰撞,只定位对象,不自动忽略碰撞;后续应补设备局部出线路径、调整 UserPath/线槽入口或检查设备装配。 +77. 面板提供“选择长接入设备”,从 `long_terminal_accesses[]` 的 `parent_device_name / parent_device_label` 反向选择设备对象,便于从设备整体高度、端子 LCS 跟随和局部出线路径三个角度排查长接入。 +78. 面板提供“选择异常导线”,从最新 `route_samples[]` 中选择所有带 `issue_codes` 的 RoutedConnection 导线,并补充扫描导线对象自身的 `QetRouteIssueCodes`,避免 compact 样例数量有限时漏选问题线。该功能是长接入、越界、容量、路径质量和碰撞问题的统一入口,只定位导线,不重新求路。 +79. 面板提供“选择异常导线路径”,从异常 `route_samples[]` 的 `carrier_names` 和 `route_track.segments[].carrier` 反向选择导线实际经过的路径 carrier,并尽量选择 carrier 的源草图/线槽对象。该功能只定位路径,不自动改 Required/Forbidden、容量或几何。 +80. 面板提供“选择选中导线路径”,从当前选中的 RoutedConnection 导线对象读取 `QetRouteTrackJson`,反向选择该线实际经过的路径 carrier 和源草图/线槽。该功能用于 compact `route_samples[]` 为空或样例不足时的单线排查,只读取 FreeCAD 文档中的导线元数据,不写数据库、不要求 QET 提供 3D 路径。 +80. 面板提供“选择拒绝兜底路径”,从当前选中的 RoutedConnection 导线对象读取 `QetRouteDiagnosticsJson.selective_collision_reroute.rejected_fallback_labels`,反向选择被局部避障重算发现但因使用 `RoutingRange` / `AuxiliaryPath` 而被拒绝的路径来源。该功能只用于判断应在哪里补 `UserPath`、桥接主路径或设备局部出线路径,不会自动接受 fallback 路线,也不写数据库。 +81. `汇总布线诊断` 会统计实际 RoutedConnection 导线对象上的 `QetRouteIssueCodes`,输出异常导线总数和各问题码数量。该统计来自 FreeCAD 文档中的导线对象,不依赖 compact `route_samples[]` 的样例上限。 +82. `汇总布线诊断` 会统计批量缺端子数量和缺失端点原因,例如 `missing_device_binding_metadata / 导线端点缺少 2D/3D 设备绑定信息`、`device_not_in_3d_scene / 该 2D 设备未在 FreeCAD 场景中找到`、`no_3d_terminals_for_element / 该 2D 设备在 FreeCAD 中没有工程端子`,用于区分 QET 端点绑定元数据缺失、设备未导入/未绑定、端子未生成和路径网络问题。 +83. `汇总布线诊断` 会根据当前问题生成手测建议动作,例如选择缺端子设备、选择异常导线、选择长接入端子/设备、选择碰撞父装配等。建议只引导 FreeCAD 面板操作,不自动修改路径或数据库。 +84. 缺端子建议会按原因码分流:`missing_device_binding_metadata` 提示检查 QET 导线端点是否提供 `element_uuid` 和 `terminal_uuid`,并明确第一版不要求 `start/end_instance_id`;`device_not_in_3d_scene` 优先提示检查设备是否已导入、装配并完成 2D/3D 绑定;`no_3d_terminals_for_element` / `no_3d_terminals_for_instance` 才提示选择缺端子设备;`terminal_uuid_not_in_element` 提示选择缺端子候选端子核对 UUID 与脚号绑定。 +85. `选择缺端子设备` 本身也会按原因码分流状态提示:如果缺失设备不在当前 FreeCAD 场景中,提示先补设备导入、装配和绑定;如果缺少 QET 端点 `element_uuid`,提示先补齐 QET 端点绑定信息,避免用户在 3D 场景中反复寻找不存在的对象。 +86. `生成布线连接` 的中文报告会根据缺端子原因追加关键提示:若包含 `missing_device_binding_metadata`,直接提示 `QET 导线端点缺少 element_uuid`,并注明第一版不要求 `start/end_instance_id`;若包含 `device_not_in_3d_scene`,直接提示部分导线引用的设备未在当前 FreeCAD 场景中找到。这个提示不依赖 `routed=0`,即使多数导线已经成功、只有少量导线缺端子,也会在报告中显示。 +87. 对旧版批量诊断中缺端子样例没有原因码的情况,汇总诊断会尝试用当前 FreeCAD 文档回填原因码:样例里有 `terminal_uuid` 但没有 `element_uuid` 或可回查的设备标识时回填为 `missing_device_binding_metadata`;样例里有设备标识但当前场景找不到设备时回填为 `device_not_in_3d_scene`;设备存在但无工程端子时回填为 `no_3d_terminals_for_element`。只有基础字段不足以判断时才显示 `缺端点原因未记录` 并建议重新生成布线连接。 +62. 对于 `missing_route_network_samples[]`、`error_samples[]`、`missing_endpoint_samples[]` 这类失败样例,`wire_object_label` 保存的是任务侧最接近对象标题的显示名,不一定已经对应到真实 3D 导线对象。 +51. 自动布线 options 支持按 carrier 名称、标签、源对象名称、源标签或类型设置禁止经过路径,Dijkstra 会跳过这些路径边。 +52. 自动布线 options 支持按 carrier 名称、标签、源对象名称、源标签或类型设置必须经过路径,Dijkstra 状态会记录必经条件并只返回满足条件的路径。 +53. 批量 `wires[]` 的单条导线任务可选携带 `required_route_carrier_*` / `forbidden_route_carrier_*` 字段,实现不同导线使用不同路径约束。 +54. 成功布线的 `QetRouteNetworkJson` 和 compact route sample 会记录 `route_constraints`,用于追踪某条线实际应用的必经/禁经规则。 +55. 批量布线中文报告和 compact 诊断会汇总路径约束使用情况,显示受 Required/Forbidden 规则影响的导线数量和样例。 +55. 批量布线后会在返回 report 和 `QETWiring_05_Diagnostics` 下的 `RoutingConnectionBatch` 诊断对象中写入同一套 `issue_codes`;诊断对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示本次批量布线是否无问题码;`QetDiagnosticIssueCodes` 直接列出问题码;`QetDiagnosticIssueLabels` 直接列出中文问题标签;`QetDiagnosticMessage` 保存中文摘要,`QetDiagnosticJson` 保存 compact 批量诊断明细,并包含当前 `runtime_version`,方便手测时确认 FreeCAD 已加载最新自动布线代码。即使当前没有导线任务,也会保留该诊断对象并提示“没有导线任务”。问题码示例包括 `no_wire_tasks`、`no_routed_connections`、`missing_terminals`、`missing_route_network`、`collision_warnings`、`route_capacity_pressure` 等,便于脚本或人工快速筛选问题。 +56. 当批量布线 `total_wires > 0` 但最终 `routed = 0` 时,中文报告会明确提示“未生成有效导线:本次只有路径承载/诊断对象,未生成 RoutedConnection 导线”。这类场景常见于端子缺失、路径网络不连通、模块版本不一致或普通布线错误,不能把树目录中的 `WireDuct`、`RoutingRange`、`TerminalAccess` carrier 当成成功布线结果。 +57. `检查布线准备度` 会记录 `runtime_capabilities`。如果当前 FreeCAD 会话加载的 `RoutingNetwork` 模块缺少路径约束收集函数,会写入 `runtime_route_constraint_collector_missing` 并提示同步运行目录、重启 FreeCAD,避免模块版本不一致导致批量布线整批失败。 +58. 外层“生成布线连接”在补齐路径网络检查、`hidden_route_carriers` 和 `routing_path_network_updated` 等最终字段后,会重写一次 `RoutingConnectionBatch` 诊断对象,保证“汇总布线诊断”读取到的是最终 report,而不是内层批量求路刚结束时的半成品诊断。 +59. compact 批量诊断会记录容量压力样例,便于定位哪条导线在哪些路径上触发“并行线数超过路径容量”。 +60. 批量布线中文报告和 compact 诊断会按 `HardIntersection` / `ClearanceWarning` 汇总碰撞分类,便于区分穿模和安全间隙不足。 +57. FreeCAD 路径 carrier 支持 `QetRouteConstraintMode = Forbidden / Required`,用于手动测试全局禁经/必经路径约束。 +58. `3D 布线连接` 面板提供“选中路径必须经过”和“选中路径禁止经过”,可直接给选中 route carrier 写入 `QetRouteConstraintMode`。 +59. `3D 布线连接` 面板提供“清除选中路径约束”,可把选中 route carrier 的 `QetRouteConstraintMode` 清空。 +60. `3D 布线连接` 面板提供“清除全部路径约束”,可一次清空当前文档中 route carrier 和源路径对象上的 Required/Forbidden 规则。 +61. 面板摘要会显示 carrier 级路径约束数量,便于确认当前 Required/Forbidden 规则是否仍在生效。 +62. 源路径对象上的 `QetRouteConstraintMode` 会在清除/重生成 carrier 后继承到新 carrier,避免路径约束刷新后丢失。 +63. 选中的草图、Draft 线和纯线状对象如果包含弧线、样条边或整条 `Wire` 拓扑,会先离散为 polyline 再生成 `UserPath`,避免曲线路径在自动布线网络中被拉直。 +64. `UserPath` 从草图、Draft 线、边或 `Wire` 提取点时会按源对象的 `Placement` / `getGlobalPlacement()` 转成文档坐标,避免装配移动后的路径仍按本地坐标生成。 +65. 同一个草图或 Draft 对象中存在多条不连通 `Wire` 时,会分别生成多条 `UserPath`,不会把第一条路径末端和第二条路径起点硬连成一条假路径。 +66. 面板和报告中的 `user_path_carriers` 表示本次生成或刷新成功的 `UserPath` 数量;重复选择同一路径对象重生成时会刷新原 carrier,并仍计入数量,便于确认操作生效。 +67. 同一个源草图生成多条 `UserPath` 时,对源草图设置 Required/Forbidden 路径约束会同步标记全部生成 carrier,避免多路径草图只约束第一条路径。 +68. 多 `Wire` 源草图刷新时,如果草图中的路径数量减少,系统会删除多余旧 `UserPath` carrier;如果路径数量增加,系统会新增对应 carrier,避免布线网络和当前黄色路径不一致。 +69. 同一个源草图生成多条 `UserPath` 时,每条 carrier 会记录 `QetRouteSourcePathIndex`,用于区分同一源对象下第几条路径,方便诊断和路径示例回溯。 +70. 通用 route carrier 创建入口也使用同样的多 `Wire` 分段规则,脚本或旧入口创建 `RoutingPath` 时不会把不连通路径硬拼成一条。 +71. 多 `Wire` 源草图设置 `QetRouteCarrierCapacity` / `QetWireCapacity` 时,生成的每条 `UserPath` 都会继承该容量,用于共路容量提示和复用成本。 +72. 创建 `UserPath` 时如果同时选中支撑面 Face 和悬空草图/Draft 线,系统会把路径点投影到该支撑面并保留默认偏移,减少 Draft 工作平面不正确导致的悬空路径。 +73. 同一个源草图生成多条 `UserPath` 时,通过面板清除选中路径约束会同时清空源草图和全部生成 carrier 的 Required/Forbidden 约束。 +74. 多 `Wire` 源草图设置 Required 时,自动布线按源对象聚合判断,经过该源对象生成的任一相关 `UserPath` 即可满足必经条件;设置 Forbidden 时,该源对象生成的全部 `UserPath` 都会被跳过。 +63. `QetRouteConstraintMode` 支持中文别名:`必须经过` / `必经` / `禁止经过` / `禁经` / `禁止`。 已完成 FreeCAD smoke: @@ -687,20 +953,22 @@ tests/manual/freecad_auto_routing_smoke.py 4. 清除走线路径 5. 点击“准备布线布局空间” 6. 按当前机柜情况调整主路径桥接容差、端子接入最大距离、端子出线长度 -7. 可选:选中无法自动识别的线槽实体 -8. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别 -9. 点击“生成布线连接” +7. 可选:选中柜内空间、柜体内腔或辅助实体,点击“选中对象作为柜内边界” +8. 可选:选中无法自动识别的线槽实体 +9. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别 +10. 点击“生成布线连接” ``` 三个按钮的职责: ```text 准备布线布局空间:识别并标记 layout space 里的线槽、支撑面、工程端子和障碍处理方式 +选中对象作为柜内边界:把选中对象包围盒标记为 CabinetInterior,只参与 3D 自动布线边界评分 生成布线路径网络:按 EPLAN routing path network 逻辑生成 WireDuct、UserPath、RoutingRange 和 TerminalAccess carrier 生成布线连接:先更新布线路径网络,再检查/绑定工程端子,按 QET 导线任务批量求路并生成 AutoSuggested 导线 ``` -如果模型名称/标签足够规范,可以不手动选择,直接执行三步;也可以只点击“生成布线连接”,系统会准备当前可识别的布线路径网络。若线槽无法自动识别,则先选中线槽实体执行“生成布线路径网络”作为补充。若甲方现场没有线槽,或需要绕开线槽自由定义柜内主路径,可以选中草图、Draft 线、线段或纯线状对象,再执行“生成布线路径网络”,系统会生成 `UserPath`。 +如果模型名称/标签足够规范,可以不手动选择,直接执行三步;也可以只点击“生成布线连接”,系统会准备当前可识别的布线路径网络。若线槽无法自动识别,则先选中线槽实体执行“生成布线路径网络”作为补充。若甲方现场没有线槽,或需要绕开线槽自由定义柜内主路径,可以选中草图、Draft 线、线段或纯线状对象,再执行“生成布线路径网络”,系统会生成 `UserPath`。若手动测试发现导线容易跑到柜外,可先选中柜内空间或辅助包围盒执行“选中对象作为柜内边界”。 ### 6.2 批量生成布线连接前提 @@ -708,16 +976,20 @@ tests/manual/freecad_auto_routing_smoke.py 2. 每条导线包含: ```text -start_instance_id 或 start_element_uuid +start_element_uuid start_terminal_uuid start_terminal_display -end_instance_id 或 end_element_uuid +end_element_uuid end_terminal_uuid end_terminal_display ``` -3. FreeCAD 文档中存在对应 `QetTerminalUuid` 的工程端子,或存在可按设备和端子显示号匹配的 `local:*` 模板端子。 -4. 布线连接只按导线任务生成,不会把场景里所有端子任意两两相连。 +`start_instance_id / end_instance_id` 不作为第一版 `wires[]` 的必填字段;FreeCAD 会通过端点 `element_uuid`、`terminal_uuid`、`devices[]` 和当前 3D 文档中的绑定属性回查 3D 实例。 + +3. 可选:单条导线可以携带 `required_route_carrier_*` / `forbidden_route_carrier_*` 路径约束字段,按 carrier 名称、标签、源标签或类型控制必须/禁止经过。 +4. 可选:QET 启动 FreeCAD 时提供 `QET_WIRE_PROPERTIES_DB=<项目数据库路径>`,或在 `2d_to_3d.json` 顶层提供 `wire_style_database_path`,或调用自动布线时传入同名 options 字段,用于按 `wire_style_id` 查询 `wire_properties` 并渲染导线颜色/线宽。 +5. FreeCAD 文档中存在对应 `QetTerminalUuid` 的工程端子,或存在可按设备和端子显示号匹配的 `local:*` 模板端子。 +6. 布线连接只按导线任务生成,不会把场景里所有端子任意两两相连。 注意:批量生成布线连接的依据是导线任务,不是“所有端子自动互连”。如果文档中只有端子而没有 `wires[]` 或 `QETWiring_01_Tasks`,系统不能判断哪些端子应该连接。 @@ -733,6 +1005,14 @@ end_terminal_display 6. 线槽是导线主路径。导线应优先从设备端子经 `TerminalAccess` 进入线槽,再沿 `WireDuct` 网络到达另一端。 7. 过线孔/穿线孔用于连接不同安装面、线槽或柜体开孔处的网络,应建模为 `WiringCutOut`,不是普通障碍。 +从甲方提供的 KYN28-12 3D 布线教程和新增截图看,成熟软件里的流程并不是“导入设备后完全自动推导所有路径”。实际操作会先完成 3D 装配和电气设备关联,再由工程人员预先绘制黄色草图路径,最后自动布线沿这些路径生成导线。也就是说,预置草图路径是正式布线输入,不是临时调试线;FreeCAD 第一版应优先保证“导入/选择已有草图路径后稳定生成 `UserPath` 并参与求路”。 + +视频里还显示门板、仪表、端子排和柜内主区域之间存在跨部件路径。例如门板上的局部梳状路径会接入一条跨门板到柜内的长主路径,端子排区域也会先形成局部短路径,再接入主路径。这说明自动布线需要支持“设备局部路径 -> 柜内主路径”的分层网络,而不是只按端子到最近线槽的一跳距离判断。 + +装配阶段和布线阶段应分开理解:端子排、仪表、按钮、导轨等对象先通过装配/配合确定最终 3D 位置;自动布线只读取最终几何位姿、工程端子、草图路径和导线任务。FreeCAD 不应让自动布线反向修改装配关系,也不应把 3D 草图路径写入第一版数据库绑定表。 + +视频中的 SW Electrical 还要求把 3D 设备和电气树中的设备/端子进行关联。对应到本项目,QET 的 `wires[]`、`terminal_uuid` 和 2D/3D 绑定仍是电气真相源;FreeCAD 侧需要在自动布线前校验工程端子是否已由这些 UUID 绑定到正确的 3D 端子对象。 + 因此,自动布线的推荐空间语义是: ```text @@ -748,9 +1028,17 @@ end_terminal_display 1. 选中草图、Draft 线、线段或纯线状对象。 2. 点击 3D 布线连接面板中的“选中路径作为用户路径”,或直接点击“生成布线路径网络”。 3. 系统把选中路径转换为 `UserPath` carrier,并参与后续自动布线最短路搜索。“选中路径作为用户路径”只创建用户路径;“生成布线路径网络”会同时更新线槽、布线面、端子接入等完整网络。 -4. 再次选择同一个路径对象生成网络时,系统会刷新原 carrier,不会重复生成。 +4. 再次选择同一个路径对象生成网络时,系统会刷新原 carrier,不会重复生成;面板报告中的 `user_path_carriers` 会把刷新成功的 carrier 也计入数量。如果多 `Wire` 草图里的路径数量减少,刷新时会删除多余旧 carrier。 5. 如果删除了原草图/线段源对象,再点击“选中路径作为用户路径”或重新生成网络,系统会清理对应的失效 `UserPath` carrier。 -6. 如果源对象设置了 `QetRouteCarrierCapacity` 或 `QetWireCapacity`,生成/刷新出的 `UserPath` 会继承该容量,用于多根线共路和容量提示。 +6. 如果源对象设置了 `QetRouteCarrierCapacity` 或 `QetWireCapacity`,生成/刷新出的 `UserPath` 会继承该容量;同一个源草图拆成多条 `UserPath` 时,每条生成路径都会继承,用于多根线共路和容量提示。 + +甲方视频里的黄色路径有两类:一类是柜内/门板区域的主路径或过渡路径,适合生成 `UserPath`;另一类是每个设备、端子排、按钮附近的短梳状局部出线路径,更适合沉淀为端子级 `TerminalAccess` 或 `QetTerminalLocalRoutePointsJson`。两者都可以来自草图,但语义不同:主路径可被多根线共享,局部路径只服务对应端子或设备附近接入。 + +SW 教程中路径可以由“创建草图”或“转换草图”得到,并且路径可能包含弧线、样条或多段折线。FreeCAD 侧处理草图路径时不能只依赖直线 `Points`;当前已支持把草图边、整条 `Wire`、Draft 线和曲线按离散精度转换为稳定 polyline,再按源对象的最终 `Placement` 转到文档坐标生成 route carrier,并保留 `QetRouteSourceName` / `QetRouteSourceLabel`,方便路径刷新和诊断回溯。这样甲方视频里的黄色曲线路径不会被简单拉直成首尾两点,也不容易因为导入对象只暴露 `Shape.Wires` 或装配移动后仍使用本地坐标而漏建、错位用户路径。若同一个草图里有多条不连通 `Wire`,系统会分别生成多条 `UserPath`,避免在两条路径之间产生并不存在的直连段。 + +已有草图路径随工程导入一起存在时,手动测试推荐流程是:先打开工程并确认黄色/草图路径在树中可见,再执行“生成布线路径网络”或“选中路径作为用户路径”,最后生成布线连接。这样比在 FreeCAD 里临时画 Draft 线更接近甲方实际流程,也能减少因工作平面选错导致的悬空路径问题。 + +如果临时画的 Draft 线或草图线明显悬空,可同时选中安装板/柜板上的支撑面 Face 和该路径对象,再执行“选中路径作为用户路径”或“生成布线路径网络”。系统会把路径点投影到支撑面附近,而不是直接使用 Draft 当前工作平面坐标。 `UserPath` 与线槽的关系: @@ -773,6 +1061,18 @@ QetTerminalLocalRoutePointsJson [[0, 0, 0], [10, 0, 0], [10, 30, 0]] ``` +也可以写成对象包装格式,便于后续模板工具附加其它元数据;当前会识别 `points`、`route_points` 或 `local_points` 三个数组键: + +```json +{ + "points": [ + {"x": 0, "y": 0, "z": 0}, + {"x": 10, "y": 0, "z": 0}, + {"x": 10, "y": 30, "z": 0} + ] +} +``` + 自动生成 `TerminalAccess` 时,系统会先把这些局部点按端子和父设备的 `Placement` 转成全局点,再从局部路径末端连接到最近的柜内主路径、线槽、用户路径或布线面。没有该字段时,仍使用原来的端子 LCS `+Z` 方向短出线。 路径网络检查也使用同一口径:如果端子有有效局部路径,端子到主路径网络的接入距离按局部路径末端计算,而不是按默认 LCS 出线点计算。这样可以避免局部路径已经接入线槽、但诊断仍误报“端子未接入”的情况。 @@ -794,7 +1094,7 @@ QetTerminalLocalRoutePointsJson 导入/更新工程端子时,FreeCAD 会把 `local_route_points` 写入该端子的 `QetTerminalLocalRoutePointsJson`。后续自动生成 `TerminalAccess` 和最终导线几何时都会使用这段局部路径。 -路径网络检查会校验端子局部路径元数据。`QetTerminalLocalRoutePointsJson` / `QetLocalRoutePointsJson` 必须是 JSON 数组,并且至少能解析出两个不同的有效点;如果 JSON 格式错误、不是数组或有效点不足,诊断对象会记录 `invalid_terminal_local_routes`,中文报告会提示“端子局部路径无效”。这类问题不会让 FreeCAD 依赖 QET 提供 3D 路径,只是提示模板端子或工程端子的 3D 局部出线元数据需要修正。 +路径网络检查会校验端子局部路径元数据。`QetTerminalLocalRoutePointsJson` / `QetLocalRoutePointsJson` 必须是 JSON 数组,或包含 `points` / `route_points` / `local_points` 数组的 JSON 对象,并且至少能解析出两个不同的有效点;如果 JSON 格式错误、没有可识别的点数组或有效点不足,诊断对象会记录 `invalid_terminal_local_routes`,中文报告会提示“端子局部路径无效”。这类问题不会让 FreeCAD 依赖 QET 提供 3D 路径,只是提示模板端子或工程端子的 3D 局部出线元数据需要修正。 如果直接在 FCStd 模板端子 LCS 上维护,也可以给模板端子写入同名属性 `QetTerminalLocalRoutePointsJson`。当前模板作者工具提供了内部函数: @@ -813,6 +1113,18 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) 4. 系统把所选路径的文档坐标转换为该端子的本地坐标,并写入 QetTerminalLocalRoutePointsJson。 ``` +如果已经在工程机柜里完成装配,也可以直接给工程端子补现场局部出线路径: + +```text +1. 在 3D 布线连接面板中,先选中一个可布线工程端子。 +2. 再选中一条表示该端子局部出线的草图、Draft 线、边或连续 Wire。 +3. 点击“选中端子设置局部出线”。 +4. 系统把所选路径从 FreeCAD 文档坐标转换为该端子的本地坐标,并写入工程端子的 QetTerminalLocalRoutePointsJson。 +5. 重新点击“生成布线路径网络”或“生成布线连接”,新的 TerminalAccess 会优先沿这段局部路径接入柜内主路径。 +``` + +这个工程端子现场设置入口只修改当前 FreeCAD 文档,不写数据库,也不要求 QET 输出 3D 路径。它适合手动测试中发现某个设备端子接入过长、从设备内部穿模、或默认 LCS 出线方向不符合实物时使用。若同一类设备会在多个项目复用,应优先把局部路径沉淀到 FCStd 设备模板里;若只是当前机柜现场微调,可以直接在工程端子上设置。 + 第一版不要求 QET 提供这个字段。它属于 FreeCAD 设备模板/工程端子的 3D 几何元数据,由 FreeCAD 模板作者维护;QET 仍只提供导线任务、设备实例、端子实例和 2D/3D 绑定所需 UUID。 ## 7. 当前限制 @@ -840,14 +1152,14 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) 2. 路径网络规模较大,但检查提示存在孤立路径网络和端子接入过长,说明部分设备局部路径没有可靠接入柜内主路径。 3. 部分导线穿过设备模型。当前碰撞检测只给出告警,不会强制阻止生成,因此在可用绕行路径不足时仍可能生成穿模导线。 4. 多根导线在公共路径上共线或高度拥挤。在线槽内共路可以接受,但在线槽外和设备端子附近需要更好的并行错位、束线显示和容量策略。 -5. 部分导线跑到机柜外侧。该问题需要后续增加柜内有效区域/柜体边界诊断,但当前优先级低于提升布通率。 +5. 部分导线跑到机柜外侧。当前已支持把柜内空间、柜体或辅助实体标记为 `CabinetInterior` 边界,并在批量报告中提示仍越出柜内区域的导线;但该能力依赖用户先标记边界,且仍需要补充柜内 `UserPath`、线槽或局部路径来提供可选的柜内路线。 6. Draft 线段可能悬空。原因通常是 Draft 当前工作平面没有锁定到安装板或线槽面;作为自由空间 `UserPath` 这是允许的,但作为贴面主路径时需要投影、吸附或明确提示。 当前开发优先级调整为: 1. 先保证更多导线能稳定布通,优先处理孤立路径网络和端子接入过长。 2. 其次降低明显穿模和线槽外共线拥挤。 -3. 柜内越界诊断放到后续阶段,不阻塞当前布通率改进。 +3. 柜内越界诊断已进入第一版收尾能力:有边界对象时会参与候选评分,并在最终路径仍越界时输出“柜内边界提示”;后续重点是让边界更容易自动识别和让用户路径更容易贴合到柜内结构。 已完成的对应改进: @@ -860,6 +1172,7 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) 7. 接入候选评分会检查端子出口到路径网络入口之间的小段是否穿过障碍包围盒;当近入口接入段穿模、稍远入口可避开障碍时,会优先选择不穿模的入口。最终碰撞诊断仍保留端点附近设备外壳的宽容规则,避免把端子自身外壳误报成碰撞。 8. 同一个路径入口已经确定后,端子出口到入口、主路径出口到端子入口的正交折线会尝试不同轴向顺序;当“先走 X”会穿设备、“先走 Y/Z”可绕开时,优先使用不穿模的折线顺序。 9. 并行导线 lane 偏移增加默认上限,避免大量导线共路时可视错位距离随 lane 序号无限增大,把导线推到线槽或柜体外。lane 序号仍保留,用于容量提示和并行数量报告。 +10. 柜内边界对象会先参与路径图过滤,再参与路径候选评分。若柜内过滤图不可达且最终路径仍存在柜内越界点,中文报告会显示“柜内边界提示”,compact 诊断会写入 `route_candidate_boundary_warning_count` 和 `route_candidate_boundary_warning_samples`。 ### 8.1 近期优先级 @@ -953,6 +1266,8 @@ PE 线优先路径 11. 两条相交或重叠的线槽中心路径能在交点/重叠端点处连通并自动拐弯。 12. 自动识别出的安装板/柜面能生成低优先级 `RoutingRange`,并可被布线连接使用。 13. 保存 FreeCAD 文档后,自动导线和路由网络仍保留。 +14. 如果 `wires[].wire_style_id` 能在 `wire_properties` 中解析,生成导线会使用对应的显示颜色、线宽和线型;解析失败时诊断显示 `Missing`,但仍按默认蓝色样式生成导线。 +15. “生成布线连接”后的 `RoutingConnectionBatch` 诊断对象保存最终 report,包括 `hidden_route_carriers`、`routing_path_network_updated`、路径网络检查结果和 `no_routed_connections` 等问题码。 ## 10. 开发验证命令 diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index 00c30d9..9ea9138 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -691,7 +691,10 @@ QETExchangeDevices 3. 点击 `生成布线连接`。 4. 查看状态中的 routed、collision_warnings、missing_terminals。 5. 若有 missing terminals,说明某些 2D 端子没有对应工程端子。 -6. 保存。 +6. 在树目录 `QETWiring_05_Diagnostics` 下查看 `RoutingConnectionBatch`。该对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示本次批量布线是否没有问题码;`QetDiagnosticIssueCodes` 直接列出问题码;`QetDiagnosticIssueLabels` 直接列出中文问题标签;`QetDiagnosticMessage` 是本次批量布线中文摘要;`QetDiagnosticJson` 是 compact 诊断明细,包含 `runtime_version`、`issue_codes`、缺失端点、碰撞、路径质量、容量、柜内边界和路径约束样例。重启 FreeCAD 后手测时,可以先看 `3D 布线连接` 面板状态摘要中的“版本:...”或诊断 JSON 里的 `runtime_version` 是否为当前开发版本,避免旧模块未刷新导致误判。 + - 真实工程批量布线还会记录 `batch_network_entry_candidate_limit`、`batch_avoid_obstacles` 和 `batch_obstacle_candidates`。前者表示批量求路时每端最多采用多少个路径入口候选,第二个字段表示是否额外构建障碍过滤路径图,第三个字段表示本次复用的碰撞障碍物候选数量;当前默认性能优先,仍会在结果中输出碰撞诊断。如果批量按钮长时间无响应,优先把这三个字段和 `route_network_carriers / route_network_segments` 一起反馈给开发侧。 +7. 如果没有导线任务,也会生成 `RoutingConnectionBatch` 诊断对象,并在 `QetDiagnosticMessage` 中提示“没有导线任务”,便于确认问题来自 QET `wires[]` 或 `QETWiring_01_Tasks`。 +8. 保存。 --- @@ -752,6 +755,237 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 19. 点击 `检查最近导线` 或 `检查全部导线`。 20. 点击 `保存并回写`。 +### 14.1 自动布线前的最小可测装配 + +如果目标是测试 `3D布线连接` 的自动布线效果,空机柜本身还不够。至少需要先完成下面的最小装配: + +1. 安装板或背板已经放入机柜,并作为设备安装基准。 +2. 导轨已经贴到安装板或背板上。 +3. 线槽已经放到柜内,或已经用草图/Draft 线定义用户主路径。 +4. QET 导入的真实设备实例已经摆到导轨或安装板上。 +5. 已点击 `生成工程端子`,工程端子能在 `QETTerminals_*` 分组中看到。 +6. 如需限制导线不能跑出柜外,选择柜内空间、柜体或辅助包围盒,点击 `选中对象作为柜内边界`。 + +完成后按下面顺序检查: + +```text +3D布线连接 + -> 检查布线准备度 + -> 准备布线布局空间 + -> 生成布线路径网络 + -> 检查布线路径网络 + -> 生成布线连接 +``` + +如果 `检查布线准备度` 显示: + +```text +路径网络:0 段 +布线源:未识别到线槽/布线面/用户路径 +柜内边界:未标记 +``` + +说明当前问题仍是装配或路径源准备不足,不应把此时的布线效果当作自动布线算法结果。 + +每次点击 `检查布线准备度`,树目录 `QETWiring_05_Diagnostics` 下会刷新一个 `RoutingPreflight` 诊断对象。该对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示预检是否通过,`QetDiagnosticIssueCodes` 直接列出问题码,`QetDiagnosticIssueLabels` 直接列出中文问题标签,`QetDiagnosticMessage` 是中文摘要;展开属性中的 `QetDiagnosticJson`,可以查看缺失端点、路径源数量、柜内边界数量、路径网络诊断和导线样式库状态。这个对象只保存最新一次预检结果,避免多次测试后诊断对象堆积。 + +`检查布线准备度` 默认不再抽样求解导线可达性,避免真实机柜中大量设备、路径 carrier 和障碍对象导致预检长时间卡住。需要排查少量导线是否能连通时,再把面板里的 `可达性抽样` 数量从 `0` 调到 1、5 或更高;这个抽样只用于诊断,不影响正式点击 `生成布线连接` 时的全量布线。 + +预检阶段也会读取路径网络诊断摘要。如果已经标记 `CabinetInterior`,但工程端子或主路径 carrier 越出柜内边界,`检查布线准备度` 会显示“路径网络检查提示”,并带出“越界端子”或“越界路径”样例。这样可以在生成导线前发现装配位置、端子 LCS 或用户路径本身的问题。 + +如果通过 QET 的 `3D` 按钮启动 FreeCAD,随后关闭它新建的工程,再手动打开已经装配好的 `FCStd`,可以直接在装配工程里点击 `检查布线准备度`。中文报告会显示 `导线来源`:显示 `QET 会话交换数据` 表示 FreeCAD 仍在读取 QET 按钮传入的 `2d_to_3d.json / wires[]`;显示 `当前 FreeCAD 文档任务` 表示读取的是当前文档中已保存的 `QETWiring_01_Tasks`;如果导线任务为 0,则需要重新从 QET 导入或确认当前会话交换数据是否还在。 + +同一份中文报告还会显示 `运行版本`。如果截图中没有 `运行版本` 或 `导线来源`,说明当前 FreeCAD 窗口仍在使用旧的 `AutoRouting.py`,需要完全关闭 FreeCAD 后重新从 QET 3D 按钮启动,再打开已装配工程。 + +如果预检发现导线端点没有匹配到 3D 工程端子,`QetDiagnosticMessage` 的“端点缺失示例”会同时显示导线标签和起终点端子,例如 `导线 N4111,terminal-start -> terminal-missing`。这样可以先回到 QET 导线任务或 FreeCAD 工程端子绑定处排查,而不必只靠 terminal UUID 猜是哪根线。 + +端点缺失明细还会显示 `FreeCAD同设备端子=N`。如果该值为 `0`,说明这根导线端点所属的 2D 设备在当前 FreeCAD 工程里没有任何 3D 工程端子,优先检查该设备是否已装配、是否已生成端子 LCS、以及 `project_2d3d_terminal_binding` 是否包含对应 `terminal_uuid`。如果该值大于 `0` 但目标端子仍缺失,通常表示同设备已有端子,但这一个端子的 `terminal_uuid` 没有绑定或不一致。 + +每次点击 `检查布线路径网络`,同一目录下会刷新 `RoutingPathNetwork` 诊断对象。该对象会保存 `QetProjectUuid`;`QetDiagnosticOk` 表示路径网络检查是否通过,`QetDiagnosticIssueCodes` 直接列出问题码,`QetDiagnosticIssueLabels` 直接列出中文问题标签,`QetDiagnosticMessage` 会直接提示空路径网络、端子未接入、端子接入过长、端子越出柜内边界、路径对象几何无效、路径越出柜内边界、孤立路径网络等问题;`QetDiagnosticJson` 保存完整诊断明细和 `issue_codes`。 + +面板中的 `端子接入警告距离 mm` 用于判断“端子接入过长”。设为 `0` 时按默认规则自动计算;如果当前机柜尺度较大,且 600-700mm 的端子接入属于可接受的设备局部出线,可以把该值调到 700mm 左右再检查。这个参数只影响质量告警,不会放宽 `端子接入最大距离 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,优先补线槽到端子主网络的桥接路径,或手动画柜内主路径后点击 `选中路径作为用户路径`。 + +`生成布线连接` 会先按路径网络诊断里的 `wire_ducts_without_terminal_access.bridge_suggestion` 自动生成一次 `UserPath` 桥接,再重新生成路径网络并开始布线。脚本或调试场景直接调用 `route_eplan_connection_tasks()` 时,也会先执行同一类诊断桥接,避免绕过面板按钮后又退回布线面兜底。报告中如果出现 `自动诊断桥接:生成 UserPath N 条`,表示系统已经自动把孤立线槽组件接入端子主网络。这个动作只修改 FreeCAD 文档中的路径 carrier,不写 QET 数据库;重复点击时已存在的同点桥接不会重复生成。 + +端子局部接入会优先连接 `WireDuct / UserPath / WiringCutOut` 等主路径;即使附近有更近的 `RoutingRange` 布线面,只要主路径仍在 `端子接入最大距离 mm` 内,系统会优先接主路径。`RoutingRange` 只作为没有线槽、没有用户主路径、或主路径距离超限时的兜底区域。这一规则用于贴近 SW/EPLAN 的工程习惯:设备端子先接入线槽/主路径,柜内大路径再沿主网络走。 + +手动桥接建议流程: + +1. 在树目录或 3D 视图中找到诊断提示的线槽 carrier,例如 `QETRouteCarrier`、`QETRouteCarrier_1`。 +2. 优先点击 `按诊断建议生成桥接`。系统会先刷新路径网络诊断,再按 `bridge_suggestion` 自动生成对应的 `UserPath` 桥接路径;如果重复点击时网络已经接通,不会再重复生成同一条桥。 +3. 如果诊断没有建议,或建议对象已经失效,再找到端子接入所在的主网络,通常是靠近设备端子、安装板或布线面的 `TerminalAccess` / `RoutingRange` carrier;如果报告已经显示“建议桥接到 xxx”,优先选中这个目标 carrier。 +4. 同时选中线槽 carrier 和目标 carrier,点击 `选中两路径生成桥接`。系统会在两条路径最近点之间生成一段 `UserPath` 桥接路径。 +5. 如果选不到已生成的 carrier,也可以选中能找到 live carrier 的源路径对象;若仍无法生成,再用草图线、Draft 线或已有线段,从线槽开口附近画到主网络附近,然后点击 `选中路径作为用户路径`。 +6. 桥接线段不需要变成实体线槽,但应落在合理柜内空间,避免悬空穿过设备。 +7. 点击 `生成布线路径网络` 或直接重新点击 `生成布线连接`。 +8. 再点击 `检查布线路径网络`,确认 `wire_ducts_without_terminal_access` 消失或数量减少。 +9. 重新生成导线后,选中导线查看 `QetRouteQualityStatus`;如果从 `FallbackPathWarning` 变成更少兜底,说明线槽接入已经改善。 + +完成 `检查布线准备度`、`检查布线路径网络` 和 `生成布线连接` 后,可以点击 `汇总布线诊断`。该按钮不会重新布线,也不会修改 QET 数据;它只读取 `QETWiring_05_Diagnostics` 下最新的 `RoutingPreflight`、`RoutingPathNetwork`、`RoutingConnectionBatch`,合并 `issue_codes` 并在面板中显示“通过/未通过、缺了哪类诊断、主要问题是什么”。同时会刷新一个 `RoutingDiagnosticSummary` 诊断对象,保存 `QetDiagnosticIssueCodes`、`QetDiagnosticIssueLabels`、`QetDiagnosticMessage` 和 `QetDiagnosticJson`,便于手动测试后在树目录中复查或复制给开发侧分析。汇总消息会显示本次诊断采用的 `runtime_version`,优先取 `RoutingConnectionBatch`,用于确认当前工程确实由最新自动布线模块生成。手动测试截图或录屏时,建议最后点一次这个按钮,方便快速判断问题来自装配准备、路径网络,还是批量布线结果。 + +如果只点击了 `生成布线连接`,没有单独点击 `检查布线准备度` 或 `检查布线路径网络`,`汇总布线诊断` 会把最新 `RoutingConnectionBatch` 当作最终诊断入口;只要批量报告内已经包含路径网络诊断摘要,就不会再因为缺少独立的 `RoutingPreflight / RoutingPathNetwork` 对象而判定失败。也就是说,真实手测可以采用“生成布线连接 -> 汇总布线诊断”的简化流程;汇总结果仍会显示端子缺失、路径网络、碰撞、柜内越界等真正问题。 + +批量报告里的 `布线布局空间` 表示本次点击时新生成或刷新的路径 carrier 数量;如果线槽路径已经存在,可能会显示 `线槽路径 0 条`。这不等于当前工程没有线槽路径。继续看下一行 `当前路径网络`,它表示本次真实参与求路的全部路径 carrier,例如 `当前路径网络:线槽路径 4 条,线槽开口 8 条,用户路径 2 条,端子接入 132 条,布线面 391 条`。判断是否识别到线槽时,以 `当前路径网络` 和 `路径采用:线槽/主路径 N 条` 为准。 + +如果旧版工程中已经存在空白的诊断对象,`汇总布线诊断` 会把它标记为 `diagnostic_json_empty / 诊断 JSON 为空`。这通常表示该诊断对象不是当前版本完整生成的,应重新执行对应步骤,例如重新点击 `检查布线准备度`、`检查布线路径网络` 或 `生成布线连接`。 + +如果工程里已有旧版 `RoutedConnection` 导线对象,但单根导线缺少 `QetRouteDiagnosticsJson`,汇总诊断会提示 `routed_wire_diagnostics_missing / 导线诊断缺失`,并给出一条导线 Label 示例。这类旧线可以显示在模型里,但无法提供碰撞、越界、接入距离等新诊断,应重新执行 `生成布线连接`。 + +如果单根导线的 `QetRouteDiagnosticsJson` 存在但不是合法 JSON,汇总诊断会提示 `routed_wire_diagnostics_invalid / 导线诊断 JSON 无效`,并给出一条导线 Label 示例。这通常表示对象来自旧版本、手工修改过属性,或导线对象保存过程中诊断字段损坏,应重新生成布线连接。 + +`RoutingConnectionBatch` 的 `QetDiagnosticJson.route_samples[]` 会保留少量样例导线。样例里可以直接看 `access`、`collision_summary`、`quality`、`capacity`、`boundary` 分组,分别对应长接入、碰撞/安全间隙、路径兜底、容量压力和柜内越界状态。手动测试后如果只想快速反馈问题,可以复制这个诊断对象 JSON;开发侧不一定需要逐根在模型里点选导线。 + +样例中的 `wire_object_label` 与左侧树目录中导线对象 Label 一致。复制诊断 JSON 给开发侧时,可以同时说明这个 Label,开发侧就能更快在 `QETWiring_04_Routed` 下定位对应导线。 + +接入距离、路径质量、候选入口碰撞风险、柜内边界、路径约束、容量压力和碰撞等 warning sample 都会尽量带 `wire_object_label`。手动反馈时优先复制这个字段,比只说“第几条线”更稳定。对于 `missing_route_network_samples[]`、`error_samples[]`、`missing_endpoint_samples[]` 这类失败样例,`wire_object_label` 是任务侧最接近对象标题的显示名,不一定已经生成出真实 3D 导线对象。 + +中文报告中,碰撞示例和缺失端点示例会优先显示 `wire_object_label`,便于直接在左侧树目录定位对象;导线样式、路径示例和统计类提示仍优先显示短导线号,避免报告过长。 + +`route_samples[]` 会优先保留有问题码的导线样例。也就是说,如果本次布线大多数线正常、少数线穿模或接入过长,压缩诊断会优先把异常线放进样例里,便于后续排查。 + +如果批量报告出现 `MissingTerminal / 缺失端子`,先看 `missing_endpoint_samples[]`。每个缺失侧会记录 `*_element_uuid`、`*_instance_id`、`*_terminal_display`、`*_device_in_scene`、`*_device_name`、`*_element_terminal_count`、`*_instance_terminal_count`、`*_missing_endpoint_reason_code` 和中文 `*_missing_endpoint_reason_label`。其中 `missing_device_binding_metadata / 导线端点缺少 2D 设备绑定信息` 表示导线任务端点缺少 `element_uuid`,FreeCAD 无法判断缺失端子属于哪个 2D 设备;第一版不要求 QET 在 `wires[]` 端点提供 `start/end_instance_id`。`device_not_in_3d_scene / 该 2D 设备未在 FreeCAD 场景中找到` 表示这条导线引用的设备当前没有对应 3D 设备实例,应优先检查该设备是否已导入、装配并完成 2D/3D 绑定;`no_3d_terminals_for_element / 该 2D 设备在 FreeCAD 中没有工程端子` 表示设备实例在场景中,但没有生成工程端子,应重新生成端子或检查模板端子。这些都不是线槽、用户路径或 Dijkstra 路径网络问题。 + +`汇总布线诊断` 会把批量报告里的缺端子信息汇总成类似 `缺端子:4 条(该 2D 设备未在 FreeCAD 场景中找到 4 处)` 或 `缺端子:2 条(该 2D 设备在 FreeCAD 中没有工程端子 2 处)` 的文本。这个统计能帮助快速判断当前问题是不是设备未导入/未绑定、设备端子未生成,还是路径网络不连通。 + +当缺端点原因是 `device_not_in_3d_scene` 时,`汇总布线诊断` 的建议会优先提示“检查缺失 3D 设备是否已导入、装配并完成 2D/3D 绑定”。这种情况下场景里没有可选中的缺失设备,不能靠调整线槽或点击路径按钮解决;应先回到设备导入、装配和绑定流程。只有原因是 `no_3d_terminals_for_element` 或 `no_3d_terminals_for_instance` 时,才优先使用 `选择缺端子设备` 去定位已存在但缺工程端子的设备。 + +当缺端点原因是 `missing_device_binding_metadata` 时,`汇总布线诊断` 的建议会提示“检查 QET 导线端点是否提供 element_uuid 和 terminal_uuid(第一版不要求 start/end_instance_id)”。这表示 FreeCAD 无法判断缺失端子属于哪个 2D 设备,应由 QET 导线任务补齐端点设备标识;第一版只要求 `wires[]` 每个端点携带 `terminal_uuid` 和 `element_uuid`,`instance_id` 由 FreeCAD 通过 `devices[]`、端子绑定或当前 3D 文档回查。 + +直接点击 `生成布线连接` 时,如果本次没有生成任何有效导线且缺端子原因包含 `missing_device_binding_metadata`,面板中文报告也会直接提示 `QET 导线端点缺少 element_uuid`,并注明第一版不要求 `start/end_instance_id`。这时不要去调整线槽、柜内边界或 Dijkstra 参数,应先检查 QET 导出的 `wires[]` 端点数据是否完整。 + +如果本次大多数导线已经成功、只有少量导线缺端子,`生成布线连接` 的中文报告也会显示 `缺端子原因提示`。例如真实工程中 `total_wires=75, routed=71, missing_terminals=4` 时,如果缺失侧设备未在当前 FreeCAD 场景中找到,报告会直接提示先检查设备导入、装配和 2D/3D 绑定,而不是只显示一条缺失样例。报告还会按设备聚合输出类似 `需补端子设备:UD:8 缺 2 处(as、sa),UD:10 缺 1 处(sa),UD:5 缺 1 处(1)` 的文本;这表示自动布线本身已经完成可布的 71 条,剩余 4 条应先补这些 3D 设备/端子场景数据,再重新生成布线连接。 + +批量诊断的 `issue_codes` 会把缺端子原因提升到顶层,例如 `device_not_in_3d_scene / 3D场景缺少设备`、`missing_device_binding_metadata / 端点缺少绑定信息`、`terminal_uuid_not_in_element / 端子UUID不匹配`、`no_3d_terminals_for_element / 设备缺少工程端子`。手测时如果看到 `device_not_in_3d_scene`,先补设备导入、装配和 2D/3D 绑定;如果看到 `terminal_uuid_not_in_element`,再用 `选择缺端子候选端子` 核对同设备端子 UUID 和脚号。 + +批量诊断还会写入 `missing_terminal_summary`。其中 `reason_code_counts` 汇总每类缺端子原因,`device_groups[]` 按缺失侧设备归并,包含 `element_uuid`、`instance_id`、缺失端子显示名、端子 UUID 和相关导线号。把该分组发给装配或绑定相关同事时,比逐条复制 `missing_endpoint_samples[]` 更容易定位需要补哪个设备。 + +这个 `missing_terminal_summary` 不只存在于 `RoutingConnectionBatch.QetDiagnosticJson`,也会直接存在于本次批量布线返回的原始 report 中。后续脚本、面板按钮或调试探针如果要拿缺设备清单,应优先读取 `report.missing_terminal_summary.device_groups[]`,不要解析中文 `需补端子设备` 文本。 + +如果布线是直接从 FCStd 里的任务对象发起,而任务对象没有保存完整设备列表,FreeCAD 会尝试从当前 QET 交换上下文的 `2d_to_3d.json` 回补 `devices[]`,用于补齐 `device_groups[].instance_id` 和设备标签。这个回补只读 JSON,不写数据库、不覆盖当前导线任务;如果项目 UUID 不一致,则不会回补。批量 report 会写入 `context_devices_loaded`、`context_device_count` 和 `context_devices_json_path`,用于判断本次是否真的加载了上下文设备列表。若缺设备分组里 `instance_id` 仍为空,优先检查当前 FreeCAD 会话是否还保留正确的交换 JSON 上下文,或环境变量 `QET_2D_TO_3D_JSON` 是否指向当前项目。 + +如果工程里保存的是旧版批量诊断,缺少 `*_missing_endpoint_reason_code / label`,`汇总布线诊断` 会尝试根据当前 FreeCAD 文档现场回填原因。只要样例里还有 `terminal_uuid`、`element_uuid` 或 `instance_id`,通常可以直接回填成“QET 端点绑定信息缺失”“设备未在 3D 场景中找到”“设备存在但无工程端子”等原因。只有样例里连这些基础字段也缺失时,才会显示 `缺端点原因未记录`,此时需要重新点击 `生成布线连接` 刷新诊断。 + +如果要快速定位这些缺端子设备,点击 `选择缺端子设备`。系统会从最新批量布线诊断的 `missing_endpoint_samples[]` 中读取缺失侧的 `*_instance_id` 和 `*_element_uuid`,并在 FreeCAD 中选中对应 3D 设备。若缺失原因是 `device_not_in_3d_scene`,当前场景里没有可选中的 3D 对象,面板会优先显示可读设备标签,例如 `UD:8`、`UD:10`,并在状态栏补充对应 `instance_id`,而不是只显示很长的 UUID;这类提示表示要先补设备导入、装配和 2D/3D 绑定。控制器返回值也会包含 `missing_terminal_device_instance_ids[]` 和 `missing_terminal_device_element_uuids[]`,便于脚本直接交给装配/绑定流程处理。该按钮只做定位,不会自动创建端子;选中后应检查该设备是否已有工程端子、端子是否来自 QET 绑定、以及设备是否装配在当前工程中。 + +如果点击 `选择缺端子设备` 后没有选中任何对象,并且诊断原因是 `device_not_in_3d_scene`,面板会直接提示缺失侧 2D 设备未在当前 FreeCAD 场景中找到。这表示没有可选中的设备对象,应先补设备导入、装配和 2D/3D 绑定;如果原因是 `missing_device_binding_metadata`,面板会提示 QET 导线端点缺少 `element_uuid`,应先补齐 QET 端点绑定信息。 + +如果缺端子样例中一端已经找到、另一端缺失,可以点击 `选择缺端子另一端`。系统会选择这些失败导线里已找到的工程端子,例如真实工程中起点缺失但终点已找到时,会选中终点端子。这个按钮用于快速确认“这根失败导线本来要连到哪里”,再回到缺失侧设备检查是否没有生成工程端子、端子脚号是否不一致或 `terminal_uuid` 是否绑定错位;它只定位端子,不会自动补端子或改 QET 数据。 + +如果缺端点原因是 `terminal_uuid_not_in_element / 同设备存在端子,但没有匹配该 terminal_uuid`,可以点击 `选择缺端子候选端子`。系统会从 `*_instance_terminal_samples` 或 `*_element_terminal_samples` 中选择同设备/同实例已有的工程端子,便于直接查看这些端子的 `QetTerminalUuid`、`QetTemplateSlotName`、`QetTerminalLabel` 和位置。若候选端子存在但 UUID 不一致,优先检查 QET 端子绑定或重新生成工程端子;若没有候选端子,则回到 `选择缺端子设备` 检查该设备是否真的生成了端子。 + +如果已经标记柜内边界,自动布线会先在柜内过滤后的路径图上求路;如果这张图不能连通两端,才回退到原始路径图。批量报告里的样例 network 会记录 `boundary_filtered` 和 `boundary_filtered_segments`,用于判断本次路线是否使用了柜内过滤图。如果某条导线仍跑出柜内区域,批量报告会提示“柜内边界提示”。选中具体导线对象时,也可以在属性里查看 `QetRouteBoundaryAware`、`QetRouteBoundaryStatus` 和 `QetRouteBoundaryViolationCount`:其中 `BoundaryWarning` 表示该导线存在柜内越界点,应优先补柜内 `UserPath`、线槽或设备局部路径。 + +导线样式来自 QET 导线任务中的 `wire_style_id`,FreeCAD 会按项目数据库 `wire_properties` 查询颜色、线宽、线型和规格文本。手动测试时,选中生成的 `RoutedConnection` 导线,可以先看 `QetWireStyleStatus`:`Resolved` 表示已查到样式,`Missing` 表示样式 ID 没查到;再看 `QetWireColorText`、`QetWireLineType`、`QetWireLineWidth`、`QetWireDiameterMm` 等属性确认原始样式数据。GUI FreeCAD 中导线会按解析到的颜色、线宽和线型显示;如果用 `FreeCADCmd` 做命令行验证,因为没有 `ViewObject`,只能确认样式属性写入,不能证明界面颜色已经显示。 + +如果 `检查布线路径网络` 提示 `route_carriers_outside_boundary / 路径越出柜内边界`,说明某条线槽中心线、`UserPath` 或过线孔路径本身已经有点落在 `CabinetInterior` 外。此时应先调整该路径源或重新标记正确的柜内边界,再生成导线;否则后续自动布线即使能连通,也容易把线带到柜外。 + +如果提示 `terminals_outside_boundary / 端子越出柜内边界`,说明至少一个工程端子的原点或出线末端落在 `CabinetInterior` 外。优先检查该设备是否真的装配到机柜内、端子 LCS 是否跟随设备实例移动,以及柜内边界对象是否过窄或标错。 + +如果要快速定位这些对象,点击 `选择越界路径/端子`。系统会从最新 `RoutingPathNetwork` 诊断里选择越界的路径 carrier 和工程端子。选中后通常按下面顺序处理:若是 `UserPath` 或线槽越界,调整源草图/线槽或重新标记正确柜内边界;若是端子越界,检查设备是否真正装配到柜内、端子 LCS 是否跟随设备移动;修正后重新点击 `检查布线路径网络` 和 `生成布线连接`。 + +如果直接点击 `生成布线连接(全部导线)`,批量报告也会带出同类路径网络检查提示。出现“越界路径:<路径标签> N 个越界点”时,优先在树目录中定位该路径源或其生成的 carrier,修正后重新生成布线路径网络和导线。出现“越界端子:<端子对象/UUID> N 个越界点”时,优先定位该端子所属设备,确认设备已经装配到柜内且端子 LCS 跟随实例移动。 + +如果多根导线共用同一路径,选中具体导线对象可以查看 `QetRouteLaneIndex`、`QetRouteLaneOffsetMm`、`QetRouteParallelWireCount`、`QetRouteMinCarrierCapacity` 和 `QetRouteCapacityStatus`。其中 `CapacityWarning` 表示这条线所在共享路径的并行线数已经超过当前路径容量,需要补备用路径、调整线槽容量或优化路径约束。 + +如果某条线已经生成但端子附近拉出很长一段斜线或折线,选中该导线对象查看 `QetRouteEntryDistanceMm`、`QetRouteExitDistanceMm`、`QetRouteAccessWarningDistanceMm` 和 `QetRouteAccessStatus`。其中 `LongAccessWarning` 表示起点或终点到主路径网络的接入距离超过当前告警阈值;`QetRouteAccessWarningSides` 会显示触发侧,`entry` 是起点侧,`exit` 是终点侧。出现该提示时,优先检查设备是否已经装配到正确位置、端子局部出线路径是否存在、用户路径或线槽是否离设备端子太远。 + +`检查布线路径网络` 和批量布线的 `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 是否随设备移动,以及设备附近是否需要补局部出线路径。 + +如果确认是某个工程端子缺少设备局部出线路径,可以直接在当前装配工程里补: + +1. 选中一个可布线工程端子。 +2. 再选中一条表示该端子出线方向的草图、Draft 线、边或连续 Wire。 +3. 点击 `选中端子设置局部出线`。 +4. 系统会把所选路径写入该工程端子的 `QetTerminalLocalRoutePointsJson`,只修改当前 FreeCAD 文档,不写 QET 数据库。 +5. 重新点击 `生成布线路径网络` 或 `生成布线连接`。 + +这个动作适合处理端子附近拉出长斜线、从设备内部穿模、或默认 LCS 出线方向不符合实物的情况。若同类设备后续会反复使用,应把局部出线路径沉淀到 FCStd 设备模板里;当前装配工程里的按钮更适合现场手动测试和个别端子的快速修正。 + +如果要确认某根线到底走了哪条线槽或黄色草图路径,选中导线对象查看 `QetRouteSourceLabels`。该属性会优先显示源路径标签;同一个草图拆成多条路径时会显示 `源路径标签(路径2)` 这类后缀。`QetRouteCarrierNames` 则显示实际经过的 carrier 对象名,适合在左侧树目录中继续定位。 + +生成导线对象在左侧树目录中的 Label 会尽量显示为 `导线号: 起点端子 -> 终点端子 (状态)`,例如 `N4111: terminal-start -> terminal-end (CollisionWarning)`。如果批量报告提示某条线有问题,可以先按导线号或端子 UUID 在 `QETWiring_04_Routed` 下定位对象,再查看它的属性。 + +选中导线对象后,也可以先看 `QetRouteIssueCodes` 和 `QetRouteIssueLabels`。这两个属性会用与批量诊断一致的问题码汇总该线自身的问题,例如端子接入过长、碰撞告警、路径质量告警、路径容量压力或柜内越界。看到问题码后,再展开对应的详细属性。 + +如果要先把本次批量布线中的问题线全部找出来,点击 `选择异常导线`。系统会从最新批量诊断 `route_samples[]` 中选择所有带 `issue_codes` 的 RoutedConnection 导线,同时也会扫描已生成导线对象自身的 `QetRouteIssueCodes`,避免 compact 样例数量有限时漏掉问题线。这个按钮适合统一排查长接入、柜内越界、容量压力、路径质量或碰撞问题;不会选择没有问题码的正常导线,也不会修改路径。 + +如果只想定位“主路径网络不够导致无法按主路径绕开碰撞”的线,点击 `选择缺主路径导线`。系统会选择带 `main_path_detour_missing` 的 RoutedConnection 导线;这类线通常表示选择性避障重算发现了可行绕法,但该绕法会退回 `RoutingRange`、`AuxiliaryPath` 或其它兜底空间路径,所以当前版本按主路径优先策略拒绝采用。选中后应优先补黄色草图 `UserPath`、桥接线槽/主路径、调整线槽入口,或给设备端子补局部出线路径,而不是直接把 fallback 结果当作正式布线。 + +如果要看这些缺主路径导线当前实际走了哪条线槽、`UserPath` 或源草图,点击 `选择缺主路径线路径`。系统会先从带 `main_path_detour_missing` 的样例中选择当前路径 carrier 和源对象,并补充扫描已生成导线对象自身的 `QetRouteIssueCodes / QetRouteTrackJson`,避免 compact 样例数量有限时漏掉问题线;其它异常线的路径不会混进来。这个按钮适合与 `选择拒绝兜底路径` 对照:前者看“现在走哪里”,后者看“算法想绕到哪里但被拒绝”,两边之间通常就是需要补主路径或局部路径的位置。 + +如果要继续定位异常导线实际经过了哪条线槽、`UserPath` 或源草图,点击 `选择异常导线路径`。系统会从最新异常 `route_samples[]` 的 `carrier_names` 和 `route_track.segments[].carrier` 中选择路径 carrier,并尽量选择其 `source_name / source_label` 对应的源对象。选中后可以检查该路径是否越界、穿模、容量不足,或直接对源路径设置 Required/Forbidden、调整容量、移动草图路径。 + +如果你已经在树目录或 3D 视图中选中了某一根问题导线,可以直接点击 `选择选中导线路径`。系统会读取该导线对象上的 `QetRouteTrackJson`,反向选择这根线实际经过的路径 carrier 和源草图/线槽;这个功能不依赖 compact `route_samples[]`,适合诊断样例为空、样例数量不足,或你想单独排查某一根穿模/越界/共线导线的场景。该操作只定位对象,不修改 QET 数据库、不重新生成导线。 + +如果这根线带 `main_path_detour_missing`,可以在选中导线后继续点击 `选择拒绝兜底路径`。系统会读取导线 `QetRouteDiagnosticsJson` 里被拒绝的 `RoutingRange` / `AuxiliaryPath` 标签,并反选对应的路径源对象;状态栏会显示 `需补路径位置`,列出前几个被拒绝的兜底路径标签。这个按钮的用途不是把兜底路线改成正式结果,而是帮助判断算法“想从哪里绕过去”:若选中的是安装板布线面或辅助路径,通常应在该区域补一条明确的黄色草图 `UserPath`、桥接到线槽/主路径,或给设备端子补局部出线路径。 + +`汇总布线诊断` 也会扫描已生成导线对象的 `QetRouteIssueCodes`,输出类似 `异常导线:2/71 条(端子接入过长 1 条、碰撞告警 1 条)` 的统计。这个数量来自实际导线对象,不受 `route_samples[]` 样例数量限制,更适合作为手测总览。 + +执行 `生成布线连接(全部导线)` 后,批量报告本身也会带出 `缺主路径绕行:N 条` 和 `需补路径位置`,不用必须再点一次汇总诊断才能看到剩余补路区域;后续若需要更完整的异常线统计,再点击 `汇总布线诊断`。如果系统还能读取到这些导线当前实际经过的主路径,会继续显示 `补路配对:兜底区域 -> 当前主路径`,例如 `FRONT DOOR-R_P00 -> WireDuct_Body001 4 条`。这表示应优先在该门板/布线面区域和对应主线槽或 UserPath 之间补桥接路径,而不是把门板兜底路线直接当成正式布线。 + +如果存在 `main_path_detour_missing`,`汇总布线诊断` 会额外显示 `缺主路径绕行:N 条`,并把这些导线诊断中被拒绝的 `RoutingRange / AuxiliaryPath` 标签汇总成 `需补路径位置`。如果单线诊断中保存了 `QetRouteTrackJson`,汇总还会显示 `补路配对`,把被拒绝的兜底区域和当前实际主路径成对列出来。这一步适合在逐根选线前先判断问题集中在哪个柜内区域、应桥接到哪条主路径;随后再点击 `选择缺主路径导线` 和 `选择拒绝兜底路径` 做单线定位。 + +如果 `补路配对` 两端都能在当前 FreeCAD 文档里找到 live carrier,可以点击 `按诊断建议生成桥接`。该按钮除了处理“线槽未接入端子主网络”的桥接建议,也会按 `兜底区域 -> 当前主路径` 配对生成一段 `UserPath` 桥接。生成的桥接对象会写入 `QetRouteBridgeKind=MainPathDetourBridge`、`QetRouteBridgePairLabel`、左右源对象 `Name/Label`,便于后续在属性面板里确认这条桥到底连接了哪两个区域。生成后重新点击 `生成布线连接(全部导线)`,检查 `main_path_detour_missing` 是否减少。若状态栏提示“未找到配对”,说明对应兜底区域或主路径源对象无法定位,需要先用 `选择缺主路径补路位置` 查看两端是否存在,或手动画 UserPath。 + +`生成布线连接(全部导线)` 默认只按当前路径网络布线,不会悄悄把诊断建议固化成新的 `UserPath`。如果现场确认某个 `main_path_detour_missing` 确实需要补主路径,可先用面板里的桥接/补路径按钮手动生成,或在脚本选项中显式开启 `auto_create_main_path_detour_bridges`。开启后,如果选择性避障已经找到一条无碰撞或碰撞更少的兜底绕行,但该绕行包含 `RoutingRange` 而被主路径优先策略拒绝,系统会先把这条已验证折线固化成 `QetRouteBridgeKind=MainPathDetourPath` 的 `UserPath`,再按 `兜底区域 -> 当前主路径` 生成 `MainPathDetourBridge`,随后只重试受影响的导线,不会整批全量重跑。批量报告中的 `auto_main_path_detour_bridges` 会记录生成数量、重试导线数和替换结果。 + +碰撞告警会自动过滤一类导入结构件误报:如果障碍物没有 QET `element_uuid`,并且位于 `QET Exchange Devices / QETCabinet / LinkGroup / Compound / NAUO` 这类导入装配上下文,同时名称或父装配命中柜体、门板、支架、盖板等结构关键词,系统会把它视为未绑定结构件,不再让导线变成 `CollisionWarning`。带 `element_uuid` 的真实设备、断路器、端子等仍然保留为碰撞告警,需要通过补局部路径、调整装配或设置路径约束处理。 + +如果只想从汇总结果直接定位这些区域,点击 `选择缺主路径补路位置`。系统会读取汇总诊断里的 `需补路径位置` 标签,并按对象 `Name / Label` 以及路径源 `QetRouteSourceName / QetRouteSourceLabel` 反选对应的 `RoutingRange / AuxiliaryPath` 来源对象;如果诊断里已经有 `补路配对`,还会同时选择当前实际主路径对应的线槽或 `UserPath` 源对象。状态栏会显示兜底区域、当前主路径和配对统计,例如 `FRONT DOOR-R_P00 -> WireDuct_Body001 4 条`。这个按钮不要求你先选中导线,适合快速查看剩余问题集中在哪个安装板、布线面或辅助路径附近,以及应桥接到哪条主路径。 + +当汇总诊断已经能读取到缺主路径绕行问题时,建议动作会按顺序提示:先点击 `选择缺主路径导线` 选中问题线,再点击 `选择缺主路径线路径` 对照当前实际路径;如果诊断中还有被拒绝的兜底路径标签,可以先点击 `选择缺主路径补路位置` 快速定位汇总需补区域,也可以在这些导线保持选中的情况下点击 `选择拒绝兜底路径` 查看单线需补路径位置。这个顺序适合处理真实工程中少量剩余碰撞线。 + +`汇总布线诊断` 还会根据当前问题给出下一步建议,例如 `点击“选择缺端子设备”定位需要补工程端子的设备`、`点击“选择异常导线”定位带问题码的导线`、`点击“选择碰撞父装配”确认结构件后再标记忽略碰撞`。手测时可以先看这一行,再决定下一步点哪个定位按钮。 + +如果要判断某根线是明显穿模还是只是距离太近,选中导线对象查看 `QetRouteCollisionStatus`、`QetRouteHardIntersectionCount` 和 `QetRouteClearanceWarningCount`。`HardIntersectionWarning` 表示导线穿过障碍包围盒,应优先改路径或设备位置;`ClearanceWarning` 表示导线没有穿过障碍,但低于安全间隙,通常需要微调路径或安全间隙参数。 + +批量诊断中的 `collision_samples[]` 也会带 `wire_object_label`。如果报告出现“碰撞示例”,可以先复制这个 Label 到树目录中查找对应导线,再结合 `collision_kind` 判断是硬碰撞还是安全间隙。碰撞样例还会带 `obstacle_parent_labels / obstacle_parent_names`,用于判断类似 `NAUO141` 这样的零件属于前门、柜体、安装板还是具体设备;确认是装配辅助件或可穿过结构后,再手动标记为忽略碰撞对象。 + +批量报告和汇总诊断里的 `top_collision_obstacles[]` 会按碰撞对象聚合高发对象,并保留 `name`、`label`、`collision_kind_counts`、`parent_labels` 和 `parent_names`。中文摘要会显示类似 `NAUO141(FRONT DOOR-R ASS'Y) 6 处`。如果高发对象属于柜体、门板、盖板或安装辅助结构,先确认导线是否真的应该穿过该区域;确认可忽略后再选择对应对象并标记为忽略碰撞。不要只因为对象名像 `NAUOxxx` 就直接全局忽略。 + +高发碰撞对象还会给出 `resolution_hint_code / resolution_hint_label`。`review_pass_through_structural_obstacle` 表示疑似柜体、门板、支架、盖板等结构件;处理方式是先在模型中定位该对象,确认它不是实际需要避让的设备实体后,选中对象点击 `选中对象忽略碰撞`,再重新生成布线。`review_device_or_layout_collision` 表示更像设备或安装区域穿模,应优先补线槽、`UserPath`、设备局部出线路径,或调整装配位置。 + +为了避免在树目录里手工查找 `NAUOxxx`,可以先点击 `选择高发碰撞对象`。系统会从最新批量布线诊断的 `top_collision_obstacles[]` 中查找对象,并在 FreeCAD 里选中这些高发碰撞对象。这个按钮只做定位和选择,不会自动忽略碰撞;确认对象确实是柜体、门板、支架或盖板等可穿越结构后,再点击 `选中对象忽略碰撞`。 + +如果诊断里已经能看到 `parent_names / parent_labels`,也可以点击 `选择碰撞父装配`。系统会直接选择高发碰撞对象所属的父装配,例如前门总成或柜体总成;确认这些总成是可穿越结构后,再点击 `选中对象忽略碰撞`,可以一次影响其下层导入子件。 + +如果汇总诊断已经把高发碰撞对象标记为 `review_pass_through_structural_obstacle / 疑似结构件可确认忽略`,可以点击 `选择结构件碰撞父装配` 先定位检查。确认这些对象确实是柜体、门板、支架、盖板等结构件后,也可以直接点击 `确认结构件忽略碰撞`。该按钮只会把诊断判定为结构件候选的最近结构父装配标记为 `PassThrough`,不会沿父链继续标记 `QET Exchange Devices` 这类工程根组,也不会标记 `review_device_or_layout_collision` 这类疑似设备或布局碰撞。标记完成后需要重新点击 `生成布线连接`,再看碰撞数量是否减少;如果剩余高发对象变成真实设备,应优先补设备局部出线路径、调整 UserPath/线槽入口或检查装配位置。 + +结构件碰撞处理后,如果 `top_collision_obstacles[]` 剩余对象的 `resolution_hint_code` 是 `review_device_or_layout_collision`,可以点击 `选择设备碰撞对象`。该按钮只定位真实设备/布局碰撞对象,不会标记 `PassThrough`,也不会修改数据库。选中后优先检查这些设备附近是否缺少局部出线路径、线槽入口是否离端子过远、用户路径是否穿过设备包围盒,或设备本身是否装配到错误位置。 + +批量诊断的 `issue_codes` 会把碰撞进一步拆成 `structural_collision_candidates / 结构件碰撞候选` 和 `device_or_layout_collisions / 设备/布局碰撞`。前者适合先定位父装配并确认是否可标记 `PassThrough`;后者不应直接忽略,应回到设备、端子局部出线路径、UserPath 或装配位置处理。 + +如果碰撞对象是导入总成下的深层子零件,例如 `门板总成 -> Compound -> NAUOxxx`,也可以选择其父装配或中间 Compound 后点击 `选中对象忽略碰撞`。当前版本会沿父装配链递归识别 `PassThrough`,因此父装配被确认可穿越后,其下层导入子件不会继续作为导线障碍。这个操作只改变 FreeCAD 文档内的布线障碍语义,不写入 QET 数据库。 + +如果要反向查看“哪些导线正在碰撞”,点击 `选择碰撞导线`。系统会从最新批量布线诊断的 `collision_samples[]` 和带 `collision_warnings` 的 `route_samples[]` 中查找 RoutedConnection 导线对象并选中。现场排查时可以先点 `选择高发碰撞对象`,再点 `选择碰撞导线`,结合 3D 视图判断是结构件误报、路径离设备太近,还是需要补局部路径。 + +如果要判断某根线是否真正走了工程主路径,选中导线对象查看 `QetRouteQualityStatus`。`NormalPath` 表示没有使用布线面/辅助路径兜底;`FallbackPathWarning` 表示路线经过了 `RoutingRange` 或 `AuxiliaryPath`,可以继续查看 `QetRouteFallbackCarrierKinds` 和 `QetRouteFallbackCarrierLabels`。这个状态不是失败,但通常说明需要补线槽、黄色草图 `UserPath`、过线孔或设备局部路径。 + +### 14.2 第一版自动布线手测验收清单 + +第一版验收不要只看“模型里有没有线”,而要同时看诊断对象和单线属性。一次有效手测至少记录下面这些结果: + +1. 面板状态摘要显示 `版本:2026-06-08-runtime-routing-v4` 或更新版本。 +2. `检查布线准备度` 能识别到 QET 导线来源、工程端子、路径网络和柜内边界;若有问题,`RoutingPreflight.QetDiagnosticIssueCodes` 能说明原因。 +3. `检查布线路径网络` 不应出现空网络、路径对象几何无效、端子越出柜内边界或路径越出柜内边界;如果出现,应先修装配或路径源。 +4. `生成布线连接` 后,`RoutingConnectionBatch.QetDiagnosticJson.runtime_version` 与面板版本一致。 +5. 有导线任务时,`RoutingConnectionBatch.routed` 应大于 0;如果为 0,应优先看 `missing_endpoint_samples`、`missing_route_network_samples` 或 `error_samples`。 +6. 正常导线的单线 `QetRouteIssueCodes` 应为空;若存在问题码,应能归类到端子接入、碰撞、柜内越界、容量压力、路径质量或路径约束。 +7. 已标记 `CabinetInterior` 时,优先要求 `QetRouteBoundaryStatus=InsideBoundary`;出现 `BoundaryWarning` 时,应补柜内主路径或修正边界。 +8. 碰撞状态优先看 `QetRouteCollisionStatus`:`NoCollision` 为理想结果,`ClearanceWarning` 可作为间隙问题记录,`HardIntersectionWarning` 视为穿模问题。 +9. 多根线共路时,检查 `QetRouteLaneIndex`、`QetRouteLaneOffsetMm` 和 `QetRouteCapacityStatus`,确认新增导线不会无诊断地贴到旧线上。 +10. 最后点击 `汇总布线诊断`,把 `RoutingDiagnosticSummary.QetDiagnosticMessage` 和主要 `issue_codes` 作为本次手测结论。 + +如果上面 1、4、5 成立,且问题导线都能通过诊断字段定位原因,说明当前版本已经具备第一版可测闭环。若仍存在导线穿模、柜外线或未布通,应优先把对应导线的 `wire_object_label`、`QetRouteIssueCodes`、`QetRouteDiagnosticsJson` 和录屏时间点一起反馈。 + --- ## 15. 常见问题 @@ -794,18 +1028,25 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 常见原因: +- 设备、线槽、导轨还没有装配到机柜内,左侧树中有对象但 3D 位姿还不是柜内真实位置。 - 没有导入线槽。 - 线槽没有标记为线槽。 - 没有从线槽实体生成中心路径。 +- 没有用草图/Draft 线创建用户主路径。 +- 没有生成 `WireDuct`、`RoutingRange`、`UserPath` 或 `TerminalAccess` carrier。 - 端子离线槽太远,缺少过渡路径。 处理: -1. 选择线槽,点击 `标记为线槽`。 -2. 打开 `3D布线连接`。 -3. 点击 `从线槽实体生成中心路径`。 -4. 点击 `扫描端子/网络`。 -5. 再尝试生成布线连接。 +1. 先把导轨、线槽和设备摆到机柜内真实位置。 +2. 选择线槽,点击 `标记为线槽`。 +3. 如果没有线槽,先画草图或 Draft 线,再点击 `选中路径作为用户路径`。 +4. 打开 `3D布线连接`。 +5. 点击 `检查布线准备度`,确认有布线源。 +6. 点击 `准备布线布局空间`。 +7. 点击 `生成布线路径网络`。 +8. 点击 `检查布线路径网络`。 +9. 再尝试生成布线连接。 ### 15.4 为什么保存后 QET 看不到 3D 位姿? diff --git a/docs/superpowers/specs/2026-05-28-电气自动布线设计.md b/docs/superpowers/specs/2026-05-28-电气自动布线设计.md index be3cf40..302c4b2 100644 --- a/docs/superpowers/specs/2026-05-28-电气自动布线设计.md +++ b/docs/superpowers/specs/2026-05-28-电气自动布线设计.md @@ -348,6 +348,6 @@ allow_floating_fallback = false 为避免第一版范围漂移,下面三项采用明确默认值,后续阶段再扩展: -1. 第一版读取 `wire_style_id` 仅用于诊断和后续扩展,不强制映射 FreeCAD 导线颜色和线宽。 +1. 第一版读取 `wire_style_id` 后会按 `wire_properties` 解析导线显示样式:能解析时映射 FreeCAD 导线颜色、线宽和线型,并把 `Resolved/Missing` 状态写入导线对象和批量诊断;缺少数据库或查不到样式时使用默认显示样式,不阻止布线。 2. 第一版只在 FreeCAD 中保存和显示自动布线长度,不导出正式长度报表。 3. 第一版不提供 `AutoSuggested` 转锁定确认导线的完整工作流;用户需要固定路径时,先使用已有手动布线能力重新创建正式手动导线。 diff --git a/docs/三期3D功能任务拆解与开发顺序.md b/docs/三期3D功能任务拆解与开发顺序.md new file mode 100644 index 0000000..99753df --- /dev/null +++ b/docs/三期3D功能任务拆解与开发顺序.md @@ -0,0 +1,848 @@ +# 三期 3D 功能任务拆解与开发顺序 + +更新时间:2026-06-08 + +## 1. 文档目标 + +本文档根据当前任务表拆解三期 3D 建模和三维布线相关功能,明确: + +- 每个任务包含哪些具体功能。 +- FreeCAD 原生是否已经具备对应能力。 +- 当前项目是否已经完成。 +- 第一版是否必须做。 +- 后续应该如何开发,以及开发顺序。 + +本文档只按当前正式路线评估: + +```text +QET / 明图CAD + -> 2d_to_3d.json + -> FreeCADExchange + -> scene.FCStd + -> 3d_to_2d.json +``` + +不再把旧 ThreeD 模块作为正式完成依据。 + +## 2. 状态定义 + +| 状态 | 含义 | +| --- | --- | +| 已完成第一版 | 当前代码已经能支撑最小可用流程,但仍可能需要优化体验和边界情况。 | +| 部分完成 | 已有基础代码或 FreeCAD 原生能力,但还没有达到任务表描述的完整交付标准。 | +| 未完成 | 当前正式 FreeCAD 路线中还没有形成可用能力。 | +| FreeCAD 原生可用 | FreeCAD 已经提供通用 CAD 能力,第一版可以直接使用,不需要重复开发。 | + +## 3. 三期 3D 建模功能交付 + +### 3.1 3D 数据模型与映射规范开发 + +任务描述: + +> 基于 QET 设备、符号、端子、项目数据库,建立设备-3D资产-场景实例-端子连接点-2D图元映射关系;定义 STEP/IGES/FCStd 资产、sidecar 元数据、设备参数、安装规则、连接点语义。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 设备实例与 3D 实例映射 | 已完成第一版 | 必须 | 使用 `project_uuid + element_uuid + instance_id`。 | QET 设备打开到 FreeCAD 后能稳定生成或复用同一个 3D 设备实例。 | +| 2D 端子与 3D 工程端子映射 | 已完成第一版 | 必须 | 第一版唯一核心依据是 `terminal_uuid`。 | 2D 端子在 FreeCAD 中能找到对应工程端子。 | +| 设备与 3D 资产映射 | 已完成第一版 | 必须 | QET 侧解析设备绑定资产,FreeCAD 侧读取 `resolved_model_path`。 | QET 绑定的 FCStd/STEP 资产能被 FreeCAD 正确导入。 | +| 场景实例保存 | 部分完成 | 必须 | 第一版不把 3D 位姿放数据库,以 `scene.FCStd` 为准。 | 设备移动后保存 `scene.FCStd`,重新打开位置不丢。 | +| STEP/IGES/FCStd 资产定义 | 部分完成 | 必须 | STEP/IGES 作为几何输入;FCStd 作为正式电气 3D 资产。 | FCStd 能保存模型几何、模板端子、工程端子和布线对象。 | +| sidecar 元数据 | 未作为主链路 | 非必须 | 当前正式路线优先 FCStd LCS,不建议第一版依赖 sidecar。 | 如后续启用,sidecar 只能作为 FCStd 之外的兼容补充。 | +| 设备参数 | 部分完成 | 非第一版必须 | 属于参数化设备库能力。 | 能用参数生成不同规格设备模型。 | +| 安装规则 | 部分完成 | 后续必须 | 如安装到导轨、安装板、柜体。 | 设备知道自己安装在哪个宿主上,移动宿主时能跟随或校验。 | +| 连接点语义 | 已改为端子语义 | 必须 | 正式叫“端子”,分模板端子和工程端子。 | 工程端子能被选中接线,带端子 UUID、位置、方向和可接线属性。 | + +### 3.2 FreeCAD 参数化设备建模能力开发 + +任务描述: + +> 基于 FreeCAD Part/PartDesign 建立电气元件参数化建模模板,支持断路器、继电器、端子排、导轨、线槽、柜体等常用结构生成;支持模型复用、尺寸参数配置、STEP/IGES 导出。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| FreeCAD 手工建模 | FreeCAD 原生可用 | 非必须二开 | 直接使用 FreeCAD Part/PartDesign。 | CAD 人员能手工建模。 | +| STEP/IGES 模型导入 | FreeCAD 原生可用,项目已接入第一版 | 必须 | 用于把厂家模型导入后保存为 FCStd。 | STEP/STP/IGES 能导入并继续添加模板端子。 | +| 给模型添加模板端子 | 已完成第一版 | 必须 | 当前端子和布线的基础。 | 用户能在模型上创建模板端子并保存为 FCStd。 | +| 断路器/继电器/端子排参数化生成 | 未完整完成 | 非当前必须 | 属于后续设备库效率能力。 | 输入极数、宽度、高度、端子数等参数后自动生成模型。 | +| 导轨/线槽/柜体参数化生成 | 部分完成 | 后续建议 | 当前已有部分基础资产和布线载体能力。 | 能按长度、宽度、高度生成导轨、线槽、柜体模型。 | +| 模型复用 | 已完成第一版 | 必须 | 复用 FCStd 模板。 | 同一个 FCStd 可在不同工程中作为设备资产使用。 | +| 尺寸参数配置 | 部分完成 | 后续建议 | 可以用 FreeCAD Spreadsheet/Expression 或 Python 生成器。 | 不改代码即可生成不同规格模型。 | +| STEP/IGES 导出 | FreeCAD 原生可用 | 非当前必须 | 主要用于对外交付几何。 | 能导出标准 STEP/IGES 文件。 | + +### 3.3 3D 资产绑定与导入管理开发 + +任务描述: + +> 在 QET 设备库中支持绑定 FreeCAD 生成模型或外部 STEP/IGES/STL 资产;提供资产路径解析、版本记录、缺失诊断、重新加载、模型元数据读取能力。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| QET 设备绑定 3D 资产 | 已完成第一版,主要在 QET 侧 | 必须 | FreeCAD 侧消费导出的模型路径。 | QET 设备能绑定 FCStd/STEP 资产。 | +| 资产路径解析 | 已完成第一版 | 必须 | `2d_to_3d.json` 提供 `resolved_model_path`。 | FreeCAD 能找到本地真实模型文件。 | +| FCStd 导入 | 已完成第一版 | 必须 | 当前正式电气 3D 资产格式。 | 导入后能读取模型、模板端子。 | +| STEP/IGES/STL 导入 | 部分完成 | 必须 STEP/IGES,STL 非重点 | STEP/IGES 只作为几何输入,STL 不适合作为电气语义资产。 | 文件能进入 FreeCAD,必要时转存 FCStd。 | +| 版本记录 | 部分完成 | 后续建议 | 至少记录文件 hash、更新时间、模板版本。 | 模型变更后能提示需要重新加载或重新生成端子。 | +| 缺失诊断 | 部分完成 | 必须 | 找不到模型文件时给出明确提示。 | 用户能知道哪个设备缺少 3D 模型文件。 | +| 重新加载 | 部分完成 | 后续建议 | 模型文件变更后刷新场景实例。 | 重新绑定或替换资产后,FreeCAD 能更新对应设备。 | +| 模型元数据读取 | 已完成第一版 | 必须 | 当前重点读取模板端子 LCS。 | 能读取端子槽位、端子类型、坐标、方向、局部出线路径。 | + +### 3.4 复杂设备结构装配开发 + +任务描述: + +> 构建机柜、安装板、DIN 导轨、线槽、设备实例的 3D 场景装配能力;支持设备拖放、吸附、对齐、旋转、偏移、安装宿主绑定和装配约束保存。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 机柜导入 | 部分完成 | 必须 | 机柜可作为场景背景和布线范围。 | QET 选择机柜后 FreeCAD 能打开对应场景。 | +| 安装板、导轨、线槽导入 | 部分完成 | 必须 | 导轨和线槽后续作为安装/布线参考。 | 能在场景中识别并显示这些载体。 | +| 设备实例装配 | 部分完成 | 必须 | 第一版允许手动摆放。 | 设备实例能放入机柜场景并保存位置。 | +| 拖放、旋转、偏移 | FreeCAD 原生可用,项目部分接入 | 必须 | 第一版可依赖 FreeCAD 原生变换。 | 用户能移动和旋转设备,保存后不丢。 | +| 吸附、对齐 | 部分完成 | 后续建议 | 需要自定义电气柜装配命令。 | 设备能自动贴合导轨、安装板或线槽边界。 | +| 安装宿主绑定 | 未完整完成 | 后续必须 | 如设备绑定到某根 DIN 导轨。 | 宿主移动时设备关系可追踪,校验时知道设备安装位置是否合法。 | +| 装配约束保存 | 部分完成 | 后续必须 | 当前主要保存 FreeCAD 位姿,不是完整约束系统。 | 重新打开后不只是位置在,还能知道设备为什么在这里。 | + +### 3.5 3D 视图导航功能开发 + +任务描述: + +> 实现 3D 视图缩放、平移、旋转、前/顶/左/右等轴测视角切换、选择高亮、实例聚焦、相机状态保存。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 缩放、平移、旋转 | FreeCAD 原生可用 | 必须但不需要二开 | 使用 FreeCAD 自带 3D 视图。 | 用户能正常查看场景。 | +| 轴测视角切换 | FreeCAD 原生可用 | 必须但不需要二开 | 使用 FreeCAD 视图命令。 | 用户能切换前/顶/左/右/等轴测视角。 | +| 选择高亮 | FreeCAD 原生可用 | 必须但不需要二开 | 端子和导线对象需要可选择。 | 选择对象时能明显看到目标。 | +| 实例聚焦 | 部分完成 | 后续建议 | 可做“定位到设备/端子/导线”命令。 | 从任务或树节点能快速定位目标。 | +| 相机状态保存 | 未完整完成 | 非第一版必须 | 属于使用体验增强。 | 重新打开工程后恢复上次视角。 | + +### 3.6 2D 到 3D 单向联动开发 + +任务描述: + +> 根据 QET 原理图/布置图中的设备、端子、柜体、导轨、线槽信息,单向生成或更新 3D 场景实例。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| QET 导出设备 | 已完成第一版 | 必须 | 导出 `devices[]`。 | FreeCAD 能得到设备实例列表。 | +| QET 导出端子 | 已完成第一版但需稳定 | 必须 | 导出 `devices[].terminals[]`。 | FreeCAD 能知道端子属于哪个设备实例。 | +| QET 导出资产路径 | 已完成第一版 | 必须 | 导出 `device_models[]`。 | FreeCAD 能找到设备 3D 模型。 | +| FreeCAD 生成/更新设备 | 已完成第一版 | 必须 | 按 `instance_id` 复用或创建设备组。 | 多次打开不会重复生成混乱设备。 | +| FreeCAD 生成/更新工程端子 | 已完成第一版 | 必须 | 依赖 FCStd 模板端子。 | 工程端子落在设备正确端子位置。 | +| 柜体、导轨、线槽联动 | 部分完成 | 后续必须 | 当前不应阻塞端子和手动布线。 | QET 中柜体/载体信息能稳定进入 FreeCAD 场景。 | + +### 3.7 3D 布线基础能力开发 + +任务描述: + +> 基于 3D 连接点和端子映射,支持线路路径采集、手动布线路径编辑、路径叠加显示、线槽/导轨空间参考,为后续自动布线预留接口。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 工程端子可被选中接线 | 已完成第一版 | 必须 | 手动布线只连接工程端子。 | 不能误选模板端子或普通几何作为接线端。 | +| 线路路径采集 | 已完成第一版 | 必须 | 起点、终点、手动路径点。 | 能记录一条导线的完整路径点。 | +| 手动布线路径编辑 | 已完成第一版 | 必须 | 添加路径点、更新导线。 | 用户能调整导线走向。 | +| 路径叠加显示 | 已完成第一版 | 必须 | 在 FreeCAD 场景中生成可见导线。 | 导线几何可见,能区分起终点。 | +| 线槽/导轨空间参考 | 部分完成 | 后续必须 | 自动布线需要,手动布线先可弱化。 | 线槽/导轨能作为布线候选路径或参考对象。 | +| 自动布线接口 | 已完成第一版 | 后续必须 | 为 qdj 自动布线提供端子、导线任务、路径网络基础。 | 自动布线可以复用工程端子和路径载体。 | + +## 4. 三期三维布线功能交付 + +### 4.1 布线数据模型设计 + +任务描述: + +> 定义端子、设备、线缆的数据结构与接口规范。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 设备数据结构 | 已完成第一版 | 必须 | `QETDevice_xxx` 设备组保存实例信息。 | 可按 `instance_id` 找到设备。 | +| 工程端子数据结构 | 已完成第一版 | 必须 | LCS 对象保存端子语义。 | 可按 `terminal_uuid` 找到工程端子。 | +| 导线任务数据结构 | 已完成第一版 | 必须 | 从 QET `wires[]` 导入。 | 任务能描述起点端子、终点端子、线号等。 | +| 已布导线数据结构 | 已完成第一版 | 必须 | FreeCAD 对象保存起终点、路径点、线长、状态。 | 保存后重开不丢,能参与回写。 | +| 线缆/多芯线数据结构 | 未完整完成 | 后续建议 | 比单根导线更复杂。 | 支持线缆、芯线、屏蔽层等关系。 | + +### 4.2 基础数据解析开发 + +任务描述: + +> 开发 3D 场景中设备、端子数据的读取与解析模块。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 读取设备实例 | 已完成第一版 | 必须 | 从 FreeCAD 文档中扫描 QET 设备组。 | 能列出当前场景设备。 | +| 读取工程端子 | 已完成第一版 | 必须 | 扫描设备下的工程端子组。 | 能按设备和端子 UUID 建索引。 | +| 读取模板端子 | 已完成第一版 | 必须 | 从 FCStd 模板 LCS 读取槽位。 | 工程端子能参考模板端子位置生成。 | +| 读取导线任务 | 已完成第一版 | 必须 | 从 `2d_to_3d.json` 的 `wires[]` 导入。 | 能得到待布线起点和终点。 | +| 端子数据稳定匹配 | 部分完成 | 必须 | 依赖 QET 提供稳定 `terminal_uuid`,可增加 `slot_name_hint`。 | 同一设备多个端子不会错位或串线。 | + +### 4.3 智能连接识别算法开发 + +任务描述: + +> 实现端子与线缆、设备接口的自动匹配识别逻辑。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 按 `terminal_uuid` 找端子 | 已完成第一版 | 必须 | 最稳定的识别方式。 | 给定端子 UUID 能找到唯一工程端子。 | +| 按槽位提示匹配模板端子 | 已完成第一版 | 必须 | 使用 `slot_name_hint` 或 `terminal_label` 辅助。 | P1/P2/A1/A2 等端子能落到正确模板槽位。 | +| 手动选择两个端子识别连接 | 已完成第一版 | 必须 | 手动布线场景使用。 | 用户选两个工程端子即可生成导线。 | +| 自动识别线缆与设备接口 | 部分完成 | 后续建议 | 属于自动布线和智能匹配。 | 系统能从导线任务自动找到起终点端子。 | +| 复杂接线关系纠错 | 未完成 | 后续建议 | 需要电气规则和 QET 数据完整支持。 | 能提示端子不匹配、线缆类型不匹配等。 | + +### 4.4 更新 BOM 并生成取线表 + +任务描述: + +> 自动更新线长、线表和 BOM 数据,生成生产用取线表。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 线长计算 | 部分完成 | 建议第一版保留字段 | 可根据导线路径点计算长度。 | 每根导线有可回写的长度。 | +| 线表生成 | 未完整完成 | 后续必须 | 需要 QET 侧线号、线型、颜色、截面积等数据。 | 能导出起点、终点、线号、长度、规格。 | +| BOM 更新 | 未完成 | 后续必须 | 需要接入 QET BOM 或物料系统。 | 导线、端子附件、线槽等物料可进入 BOM。 | +| 取线表 | 未完成 | 后续必须 | 面向生产下线。 | 可按柜体、线号、长度批量输出。 | + +### 4.5 电气布线规则梳理 + +任务描述: + +> 明确布线的电气规范,如线距、转角、分层规则等。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 基础线距/避障规则 | 部分完成 | 必须基础版 | 自动布线已有部分间距参数。 | 导线不会明显穿过设备或柜体。 | +| 转角规则 | 部分完成 | 建议 | 当前可用正交路径表达。 | 路径转角符合甲方要求。 | +| 分层规则 | 未完整完成 | 后续必须 | 如强弱电、不同电压等级分层。 | 不同线缆类别按规则走不同区域。 | +| 线槽容量规则 | 部分完成 | 后续必须 | 当前已有容量/复用思路,但未完整产品化。 | 超容量时提示或改道。 | +| 甲方规范配置 | 未完成 | 必须依赖甲方 | 规则需要甲方确认。 | 规则可配置、可验收。 | + +### 4.6 路径规划算法开发 + +任务描述: + +> 基于规则实现自动生成无碰撞、合规的布线路径。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 端子到端子路径生成 | 已完成第一版 | 后续自动布线必须 | 自动布线使用工程端子作为起终点。 | 能从起点端子生成到终点端子的 3D 路径。 | +| 正交路径生成 | 已完成第一版 | 必须基础版 | 符合柜内布线常见表达。 | 导线由水平/垂直/深度方向线段组成。 | +| 线槽/路径网络布线 | 已完成第一版 | 后续必须 | 依赖 RoutingNetwork。 | 导线能优先走线槽或用户定义路径。 | +| 避障 | 部分完成 | 后续必须 | 已有包围盒避障和碰撞检查。 | 不穿设备、不穿柜体、不超出布线区域。 | +| 合规评分与择优 | 部分完成 | 后续必须 | 当前仍需优化。 | 多条候选路径中选择更符合规则的一条。 | + +### 4.7 算法性能优化与测试 + +任务描述: + +> 优化路径生成效率,处理复杂场景下的布线稳定性问题。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 单根导线性能 | 部分完成 | 必须 | 当前先保证单根或少量导线可用。 | 单根导线生成不卡顿。 | +| 批量导线性能 | 未完整完成 | 后续必须 | 大量导线需要缓存和分批处理。 | 大工程批量布线耗时可接受。 | +| 复杂场景稳定性 | 未完整完成 | 后续必须 | 需要机柜、设备、线槽组合测试。 | 多设备、多线槽、多导线时不崩溃、不乱连。 | +| 自动测试用例 | 未完整完成 | 后续必须 | 需要固定样例工程。 | 每次修改后能跑回归测试。 | + +### 4.8 手动调整功能开发 + +任务描述: + +> 实现布线路径的拖拽、修改、撤销/重做等交互功能。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 设置起点端子 | 已完成第一版 | 必须 | 从选择对象中识别工程端子。 | 起点不是工程端子时给出提示。 | +| 设置终点并生成 | 已完成第一版 | 必须 | 连接两个工程端子。 | 生成导线对象并保存起终点 UUID。 | +| 添加手动路径点 | 已完成第一版 | 必须 | 控制导线走向。 | 导线按用户路径点生成。 | +| 修改已布导线 | 部分完成 | 必须 | 对已有导线重新生成。 | 调整后不重复生成错误导线。 | +| 拖拽路径点 | 部分完成 | 后续建议 | 更偏 UI 体验。 | 通过鼠标拖拽调整导线路径。 | +| 撤销/重做 | 部分依赖 FreeCAD 原生 | 后续建议 | 可先依赖 FreeCAD 文档操作栈。 | 用户误操作可以恢复。 | + +### 4.9 实时错误检查逻辑开发 + +任务描述: + +> 开发布线冲突、连接错误、电气规范违规的实时检测。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| 非端子选择检查 | 已完成第一版 | 必须 | 手动布线时必须限制对象类型。 | 选错对象时不给生成导线。 | +| 起终点相同检查 | 已完成第一版 | 必须 | 避免自连接。 | 同一端子不能生成导线。 | +| 缺失端子检查 | 部分完成 | 必须 | 自动布线和任务导入都需要。 | 找不到端子时给出明确原因。 | +| 碰撞检查 | 部分完成 | 后续必须 | 当前已有碰撞/间隙诊断雏形。 | 导线与设备冲突时能提示。 | +| 电气规则违规检查 | 未完整完成 | 后续必须 | 依赖完整规则库。 | 强弱电混走、线径不匹配等可检测。 | +| 真正实时检查 | 部分完成 | 后续建议 | 当前更接近命令执行后检查。 | 用户调整路径时实时刷新错误提示。 | + +### 4.10 整体联调与验收 + +任务描述: + +> 全流程功能联调,修复 bug 并完成验收测试。 + +| 功能 | 当前完成情况 | 是否第一版必须 | 开发说明 | 完成标准 | +| --- | --- | --- | --- | --- | +| QET 到 FreeCAD 打开链路 | 已完成第一版 | 必须 | QET 3D 视图打开 FreeCAD。 | 能打开对应工程和场景文件。 | +| 设备导入链路 | 已完成第一版 | 必须 | FreeCAD 根据 QET 数据导入设备。 | 设备模型能正常显示。 | +| 端子生成链路 | 已完成第一版但需继续稳定 | 必须 | 模板端子生成工程端子。 | 工程端子位置准确,不错位。 | +| 手动布线链路 | 已完成第一版 | 必须 | 两个工程端子生成导线。 | 保存重开后导线不丢。 | +| 自动布线链路 | 部分完成 | 后续必须 | qdj 负责继续完善。 | 批量线缆能按规则生成路径。 | +| BOM/取线表链路 | 未完成 | 后续必须 | 需要 QET 侧数据和导线长度回写。 | 可生成生产可用线表。 | +| 验收测试 | 未完成 | 必须 | 需要固定工程和操作用例。 | 每项任务有可演示、可复现的验收步骤。 | + +## 5. 当前 zwl 应优先负责的范围 + +当前 zwl 的重点不是整张任务表,而是下面这条 FreeCAD 端子和手动布线链路: + +```text +FCStd 模板端子 + -> 工程端子生成 + -> 工程端子显示/选择 + -> 手动连接两个工程端子 + -> 生成 3D 导线 + -> 保存到 scene.FCStd + -> 生成 3d_to_2d.json 回写 +``` + +### 5.1 zwl 当前必须做 + +| 功能 | 说明 | 完成标准 | +| --- | --- | --- | +| 模板端子规范 | 定义设备 FCStd 内哪里可以接线。 | STEP/FCStd 模型能保存模板端子、槽位名和出线方向。 | +| 模型元数据读取 | FreeCAD 读取 FCStd 内模板端子。 | 导入设备后能读取模板端子位置和方向。 | +| 工程端子生成 | 从模板端子生成项目内可接线端子。 | 工程端子落在设备正确位置。 | +| 端子全局坐标计算 | 设备移动/旋转后仍能得到正确端子位置。 | 导线起终点跟随设备变化。 | +| 手动布线 | 选两个工程端子生成导线。 | 导线几何正确,起终点 UUID 正确。 | +| 手动路径点 | 支持用户控制导线路径。 | 路径点保存后重开不丢。 | +| 导线保存 | 导线对象保存到 `scene.FCStd`。 | 关闭重开工程,导线仍存在。 | +| 3D 回写 | 生成 `3d_to_2d.json`。 | 回写设备实例、端子绑定、导线基础数据。 | +| 基础错误检查 | 检查选错对象、缺失端子、起终点相同。 | 错误操作有明确提示,不生成错误导线。 | + +### 5.2 zwl 当前不应作为主线做 + +| 功能 | 原因 | +| --- | --- | +| 完整参数化设备库 | 当前可先用已有 STEP/FCStd 资产加端子。 | +| 断路器/继电器/端子排通用参数模板 | 属于提高资产制作效率的后续能力。 | +| QET 设备库资产绑定界面 | 主要在 QET 侧。 | +| 完整复杂装配约束 | 当前只要求端子和导线稳定。 | +| 完整自定义 3D 视图导航 | FreeCAD 原生能力够第一版使用。 | +| 自动布线核心算法 | qdj 负责,依赖 zwl 的工程端子和手动布线基础。 | +| BOM/取线表 | 后续基于导线长度和端子回写再做。 | +| 完整电气规则库 | 需要甲方规则和整体设计。 | + +## 6. 推荐开发顺序 + +### 阶段 1:端子资产基础 + +目标:让一个普通 STEP 模型变成可接线的 FCStd 电气资产。 + +开发顺序: + +1. 完善模板端子创建和校验。 +2. 明确模板端子属性:槽位名、端子类型、是否可接线、出线方向。 +3. 支持把模板端子的局部出线路径保存进 FCStd。 +4. 保存为可复用 FCStd 设备模板。 + +验收: + +- 打开一个 STEP 电流互感器模型。 +- 添加 P1/P2 等模板端子。 +- 保存为 FCStd。 +- 重新打开后模板端子仍然存在,属性不丢。 + +### 阶段 2:工程端子生成 + +目标:QET 打开 FreeCAD 后,设备实例上有可用于接线的工程端子。 + +开发顺序: + +1. 导入 QET 设备实例。 +2. 读取设备 FCStd 内模板端子。 +3. 根据 QET `terminal_uuid` 生成工程端子。 +4. 如果 QET 没有端子 UUID,支持生成 `local:*` 本地工程端子用于 3D 验证。 +5. 设备移动/旋转后,工程端子全局坐标正确。 + +验收: + +- 一个设备实例能生成工程端子。 +- 两个同型号设备实例的工程端子分别落在各自设备上。 +- 移动设备后,端子位置跟随变化。 + +### 阶段 3:手动布线 + +目标:用户能连接两个工程端子。 + +开发顺序: + +1. 选择工程端子作为起点。 +2. 选择工程端子作为终点。 +3. 根据端子全局坐标生成导线。 +4. 支持添加手动路径点。 +5. 保存导线属性:起点端子 UUID、终点端子 UUID、路径点、线长、状态。 +6. 提供基础错误提示。 + +验收: + +- 选两个端子能生成一根导线。 +- 不能连接模板端子或普通几何。 +- 起终点不能相同。 +- 导线显示正确,不乱连到已有导线。 + +### 阶段 4:保存和回写 + +目标:3D 布线结果可以保存和交还 QET。 + +开发顺序: + +1. 导线、端子、设备保存在 `scene.FCStd`。 +2. 重新打开工程时恢复端子和导线。 +3. 生成 `3d_to_2d.json`。 +4. 回写设备实例、端子绑定、导线基础信息。 +5. 跳过本地端子或无法可靠回写的端子,并给出说明。 + +验收: + +- 保存关闭再打开,导线不丢。 +- `3d_to_2d.json` 中能看到实例、端子和导线信息。 +- QET 能消费回写结果或至少不报错。 + +### 阶段 5:基础诊断和联调 + +目标:让 CAD 人员使用时不会因为错误操作把工程弄乱。 + +开发顺序: + +1. 检查缺失模型。 +2. 检查设备没有模板端子。 +3. 检查端子 UUID 缺失。 +4. 检查工程端子错位。 +5. 检查导线起终点无效。 +6. 输出清晰提示。 + +验收: + +- 出错时能告诉用户是哪台设备、哪个端子、什么原因。 +- 不产生半成品错误对象。 + +### 阶段 6:装配和布线增强 + +目标:提升机柜内真实装配和走线质量。 + +开发顺序: + +1. 设备吸附到导轨/安装板。 +2. 保存安装宿主关系。 +3. 线槽/导轨作为布线载体。 +4. 导线优先沿线槽或用户路径走线。 +5. 增加碰撞和越界检查。 + +验收: + +- 设备能稳定放在导轨或安装板上。 +- 导线能沿线槽走,不明显穿设备。 + +### 阶段 7:自动布线、BOM 和取线表 + +目标:在手动布线稳定后,扩展自动化生产能力。 + +开发顺序: + +1. qdj 基于工程端子和布线载体开发自动布线。 +2. 引入甲方电气布线规则。 +3. 批量生成导线。 +4. 计算线长。 +5. 回写线表、BOM、取线表。 +6. 做大工程性能优化和验收测试。 + +验收: + +- 一批导线能自动生成。 +- 有冲突能提示。 +- 能输出生产可用取线表。 + +## 7. 第一版最小交付清单 + +第一版不要求完成整张任务表,只要先完成下面这些: + +1. FCStd 模板端子制作。 +2. FCStd 模板端子读取。 +3. 工程端子生成。 +4. 工程端子选择。 +5. 两个工程端子生成手动导线。 +6. 手动路径点保存。 +7. 导线保存到 `scene.FCStd`。 +8. 重新打开不丢端子和导线。 +9. 生成 `3d_to_2d.json`。 +10. 基础错误提示。 + +完成这 10 项,就可以对外说: + +> FreeCAD 端子显示、端子手动连线、保存回写第一版完成。 + +## 8. 甲方资料补充分析 + +资料来源: + +- `D:\video\甲方视频\3D布线功能需求开发文档.docx` +- `D:\video\甲方视频\3D布线KYN28-12操作教程(终版).mp4` +- `D:\video\甲方视频\甲方布线操作.mp4` +- `D:\video\甲方视频\保护装置模型视频.mp4` +- `D:\video\甲方视频\面板装配、路径绘制、导轨配合038F3440.MP4` +- `D:\downloadWX\xwechat_files\wxid_pv577xuccot722_5d4a\msg\file\2026-06\20260601_10365403921E50.MP4` + +甲方当前参考软件是 SOLIDWORKS Electrical 3D。设计时也应参考 EPLAN Pro Panel 的电气柜 3D 安装布局和布线思想。 + +### 8.1 甲方需求归纳 + +Word 需求文档中核心需求可以归纳为 6 类: + +| 序号 | 甲方需求 | 对应我们系统的含义 | 当前优先级 | +| --- | --- | --- | --- | +| 1 | 三维模型读取与关系配合 | 导入柜体、安装板、面板、设备,并能做装配配合。 | 高 | +| 2 | 配合面定义与快速装配 | 设备能快速安装到导轨、安装板、面板等基准上,支持端子排、小型断路器批量插入。 | 中高 | +| 3 | 三维零件建模与电气脚点定义 | STEP/FCStd 设备需要补电气端子,端子带脚号、方向、可接线属性。 | 高 | +| 4 | 草图路径自定义与布线规则 | 用户可在 3D 空间画线段/曲线作为布线路径,导线可沿这些路径或区域走线。 | 高 | +| 5 | 取线表数据生成与导出 | 从布线结果生成线长、线缆规格、颜色、线耳、源/目标设备脚号等生产数据。 | 后续高 | +| 6 | 错误自检与线束高亮提示 | 检查缺失、未连接、路径冲突,选中线束时高亮并提示源/目标信息。 | 中高 | + +### 8.2 从视频看到的实际操作意图 + +| 视频 | 观察到的重点 | 对我们开发的启发 | +| --- | --- | --- | +| `面板装配、路径绘制、导轨配合038F3440.MP4` | 安装板/面板是柜体结构的一部分,通过面、边、孔或基准与柜体配合;导轨、端子排、设备再装到面板上。 | FreeCAD 侧需要把安装板/面板作为“结构载体对象”,支持配合面、安装面、孔位参考和路径对象。 | +| `甲方布线操作.mp4` | 先插入设备、打开柜体或装配体,再做设备端子、路径绘制、布线效果展示。 | 我们应先确保设备、工程端子、路径对象、导线对象这四类对象的语义稳定。 | +| `3D布线KYN28-12操作教程(终版).mp4` | 有保存在线缆工程目录、装配完成状态、草图路径、端子排线槽配合、最终布线效果。 | 路径草图和线槽/导轨配合不是装饰几何,应成为布线网络或布线参考。 | +| `保护装置模型视频.mp4` | 保护装置模型带前面板、后部结构和端子区域。 | 设备模板端子不能只按包围盒猜点,必须由模板端子或连接点模式明确指定。 | +| `20260601_10365403921E50.MP4` | 显示机柜内端子排、导轨、线槽、局部装配和设备位置参考。 | 蓝色 CAD 参考线或草图线应区别于最终布线导线,作为安装/路径参考。 | + +### 8.3 SW/EPLAN 对标结论 + +SOLIDWORKS Electrical 3D 的核心思想: + +- 2D 电气原理图和 3D 柜体布局联动。 +- 3D 中管理设备布局,并布置 wires、cables、harnesses。 +- 通过 routing paths / ducts 引导线缆路径。 +- routing 完成后更新线长,并可生成包含长度的报表。 +- routing cables 时需要 origin / destination;routing wires 时起终点来自电气图。 + +EPLAN Pro Panel 的核心思想: + +- 在布局空间中放置柜体、安装板、安装导轨、电缆槽和设备。 +- 机械元件可以有安装面,设备放在安装面或导轨上。 +- 3D 部件放置上的连接点可以图形化显示,并显示连接点方向。 +- 布线需要布线路径网络、布线范围、连接点方向和待布线连接。 +- 已布线连接会计算长度,可用于生产和报表。 + +对我们项目的结论: + +```text +安装板/面板/导轨/线槽 + 不只是普通模型 + -> 应该成为 3D 装配和布线的结构载体 + +设备端子 + 不只是几何点 + -> 应该成为带 terminal_uuid、槽位名、方向、脚号的工程端子 + +蓝色/彩色草图路径 + 不应该当作最终导线 + -> 应该作为布线路径网络、布线范围或安装参考 + +最终导线 + 必须连接两个工程端子 + -> 保存起点、终点、路径、线长、规格、状态 +``` + +### 8.4 对当前 FreeCAD 开发的影响 + +| 功能 | 是否 FreeCAD 原生已有 | 我们要做什么 | +| --- | --- | --- | +| 导入柜体、安装板、导轨、线槽 | FreeCAD 原生能导入几何 | 给这些对象补业务类型,如 Cabinet、MountingPlate、DinRail、WireDuct、RoutingPath。 | +| 装配配合 | FreeCAD 有基础移动/约束能力,但不是电气柜业务规则 | 做轻量配合语义:安装面、宿主、局部坐标、吸附结果保存。 | +| 设备放置 | FreeCAD 能移动设备 | 增加“设备属于哪个安装面/导轨”的语义。 | +| 设备端子 | FreeCAD 没有电气端子语义 | 用 FCStd LCS 模板端子和工程端子实现。 | +| 连接点方向 | FreeCAD 有坐标系方向 | 规定 LCS 本地 +Z 为出线方向,并在布线时使用。 | +| 草图路径 | FreeCAD 能画草图/线段 | 把草图/边/线转换为 RoutingPath,供布线算法使用。 | +| 布线范围 | FreeCAD 没有电气布线范围语义 | 需要新增 RoutingArea 或 CabinetRoutingZone 概念。 | +| 线长/取线表 | FreeCAD 没有 QET 生产数据 | FreeCAD 计算路径长度,QET 或后处理生成生产取线表。 | +| 错误自检 | FreeCAD 没有电气规则检查 | 我们需要检查缺失端子、路径断开、碰撞、越界、线槽容量等。 | + +### 8.5 需要 QET 侧配合的内容 + +以下内容如果涉及改 QET 代码,需要找 QET 对应开发者配合: + +| QET 侧能力 | 为什么需要 | FreeCAD 侧依赖 | +| --- | --- | --- | +| 稳定导出设备 `element_uuid` 和 `instance_id` | FreeCAD 用它找到设备实例。 | 设备导入和回写。 | +| 稳定导出端子 `terminal_uuid` | FreeCAD 工程端子绑定 2D 端子的唯一依据。 | 工程端子生成、导线起终点绑定。 | +| 导出端子显示名或槽位提示 | 防止同一设备多个端子顺序错位。 | `slot_name_hint` / `terminal_label` 匹配模板端子。 | +| 导出导线任务 `wires[]` | 自动布线和待布线列表需要。 | 起点端子、终点端子、线号、线型。 | +| 导出线缆规格、颜色、截面积 | 取线表和导线显示需要。 | 线长、线色、线径、线耳。 | +| QET 设备库绑定 FCStd 资产 | FreeCAD 需要知道每个设备用哪个 3D 模型。 | 资产导入。 | +| QET 消费 `3d_to_2d.json` | FreeCAD 回写结果要进入 QET。 | 实例绑定、端子绑定、线长/布线结果。 | +| 取线表格式定义 | 最终要给终端取线机读取。 | FreeCAD 只提供线长和路径基础数据。 | + +### 8.6 新增建议开发顺序 + +结合甲方资料,推荐把后续顺序调整为: + +1. 端子模板和工程端子稳定。 +2. 手动布线和保存回写稳定。 +3. 草图/边/线转换为 `RoutingPath`。 +4. 安装板/面板/导轨/线槽标记为结构载体。 +5. 设备和结构载体建立轻量配合关系。 +6. 自动布线使用 `RoutingPath`、线槽、布线范围。 +7. 错误自检和高亮。 +8. 线长、线缆属性、取线表。 + +其中第 1、2 项是 zwl 当前主线;第 6 项已经在另一个会话中推进;第 8 项需要 QET 和生产数据格式共同确定。 + +### 8.7 第一版建议不要扩大到的范围 + +为了避免 FreeCAD 侧失控,第一版暂不建议做: + +- 完整替代 SW 的机械装配 Mate 系统。 +- 完整参数化设备库。 +- 完整自动端子排生成和编号规则。 +- 完整取线机格式导出。 +- 完整 EPLAN 级规则库。 + +第一版只要把下面链路跑通: + +```text +FCStd 电气资产 + -> 模板端子 + -> 工程端子 + -> 结构载体/布线路径参考 + -> 手动或自动布线 + -> 线长和错误诊断 + -> scene.FCStd + 3d_to_2d.json +``` + +### 8.8 甲方取线表样例分析 + +取线表样例: + +- `D:\downloadWX\xwechat_files\wxid_pv577xuccot722_5d4a\msg\file\2026-04\PT2柜取线表.xlsx` + +工作簿结构: + +| 工作表 | 行列情况 | 说明 | +| --- | --- | --- | +| `线束组件编码` | 1 行 1 列 | 当前样例中只有标题。 | +| `取线表` | 528 行 52 列 | 真实取线表数据。 | + +主要字段可以分成 5 类: + +| 字段类别 | 代表字段 | 来源判断 | 说明 | +| --- | --- | --- | --- | +| 连接两端信息 | `号码管字符1`、`号码管方向1`、`线鼻子型号1`、`剥皮长度1`、`号码管字符2`、`号码管方向2`、`线鼻子型号2`、`剥皮长度2` | QET 为主,FreeCAD 可补位置/区域 | 这些是电气连接和生产加工字段,不能只靠 3D 几何推断。 | +| 导线规格信息 | `导线型号`、`导线颜色`、`导线截面积mm2` | QET 为主 | 来自 2D 图纸、导线样式、线缆规则或物料数据。 | +| 线长信息 | `导线长度(mm)`、`加工总数` | FreeCAD 计算,QET 汇总 | FreeCAD 按 3D 路径计算实际长度,QET 或后处理计算生产数量和汇总。 | +| 生产数量信息 | `生产总数`、`单批数量`、`套数`、`加工总数` | QET/生产系统为主 | 依赖项目数量、批次、套数,不应由 FreeCAD 单独决定。 | +| 区域和线束编号 | `区域`、`始端区域`、`末端区域`、`分线束编号`、`总线束编号`、`小区域` | QET + FreeCAD | QET 知道柜体/图纸区域,FreeCAD 可根据 3D 设备所在柜内区域辅助计算。 | + +从样例看,取线表最终需要的不只是“线长”,而是一条完整生产记录: + +```text +起点设备/端子/号码管/线鼻子 + + 终点设备/端子/号码管/线鼻子 + + 导线型号/颜色/截面积 + + 3D 路径计算线长 + + 所属区域/线束编号/生产数量 +``` + +因此推荐职责边界: + +| 职责 | FreeCAD 侧 | QET 侧 | +| --- | --- | --- | +| 端子空间位置 | 负责 | 消费结果或仅保存绑定 | +| 实际 3D 走线路径 | 负责 | 可读取回写 | +| 线长计算 | 负责 | 汇总、修正、导出 | +| 线号/号码管字符 | 不负责生成,最多回传关联端子 | 负责 | +| 导线型号/颜色/截面积 | 不负责主数据 | 负责 | +| 线鼻子/剥皮长度 | 不负责主数据 | 负责 | +| 区域/柜体/小区域 | 可根据 3D 空间辅助判断 | 负责主数据和最终分类 | +| 取线表格式导出 | 可提供基础 JSON | 负责最终 Excel/取线机格式 | + +### 8.9 当前 JSON 交换样例分析 + +当前工程交换目录示例: + +```text +D:\test\MT\电气工程4.0\电气工程414\电气工程414\.qet_freecad + 2d_to_3d.json + 3d_to_2d.json +``` + +当前 `2d_to_3d.json` 统计: + +| 内容 | 数量 | 说明 | +| --- | --- | --- | +| `devices[]` | 86 | QET 导出的 2D 设备实例和端子上下文。 | +| `device_models[]` | 85 | QET 解析出的 3D 资产路径。 | +| `wires[]` | 75 | QET 导出的导线任务。 | +| `stale_devices[]` | 27 | 已失效或不在当前快照中的设备。 | +| `cabinet` | 1 | 当前图纸绑定的机柜上下文。 | + +当前 `wires[]` 已有字段示例: + +```json +{ + "start_element_uuid": "...", + "start_terminal_uuid": "...", + "start_terminal_display": "12", + "end_element_uuid": "...", + "end_terminal_uuid": "...", + "end_terminal_display": "21", + "wire_id": "direction:...", + "wire_mark": "N4131", + "wire_mark_is_manual": false, + "wire_style_id": 3 +} +``` + +这些字段已经足够支持“从 QET 导线任务找到 FreeCAD 工程端子并生成导线”。 + +但对取线表还不够,缺少: + +| 取线表需要 | 当前 JSON 是否已有 | 建议责任 | +| --- | --- | --- | +| 导线型号 | 未直接看到,可能需通过 `wire_style_id` 回查 | QET 补充或回查 | +| 导线颜色 | 未直接看到,可能需通过 `wire_style_id` 回查 | QET 补充或回查 | +| 导线截面积 | 未直接看到,可能需通过 `wire_style_id` 回查 | QET 补充或回查 | +| 线鼻子型号 | 未看到 | QET 提供 | +| 剥皮长度 | 未看到 | QET 提供 | +| 号码管字符 | 部分可由 `wire_mark`、端子显示、设备信息组合 | QET 负责最终生成 | +| 3D 实际线长 | 当前 `2d_to_3d.json` 不应有,应该 FreeCAD 回写 | FreeCAD 计算 | +| 分线束编号/总线束编号 | 未看到 | QET/生产规则提供 | +| 起终点区域 | 当前有柜体上下文,但不完整 | QET 主导,FreeCAD 可辅助空间判断 | + +当前 `3d_to_2d.json` 统计: + +| 内容 | 数量 | 说明 | +| --- | --- | --- | +| `instances[]` | 85 | FreeCAD 回写设备实例绑定。 | +| `terminals[]` | 138 | FreeCAD 回写端子绑定。 | + +当前 `3d_to_2d.json` 尚未包含: + +- 已布导线列表。 +- 每根导线的 3D 路径点。 +- 每根导线的实际长度。 +- 碰撞/错误状态。 +- 线缆/导线规格回传字段。 + +因此如果目标是最终生成甲方取线表,需要新增 FreeCAD 回写结构,建议命名为: + +```json +{ + "routed_wires": [ + { + "wire_id": "string", + "wire_mark": "string", + "start_terminal_uuid": "string", + "end_terminal_uuid": "string", + "start_instance_id": "string", + "end_instance_id": "string", + "length_mm": 1234.5, + "route_status": "Routed", + "route_mode": "Manual", + "route_points": [], + "collision_count": 0, + "diagnostics": [] + } + ] +} +``` + +注意:`routed_wires[]` 只负责把 3D 布线结果回写给 QET。最终取线表中的导线型号、颜色、截面积、线鼻子、剥皮长度、号码管字符、区域、生产数量,仍建议由 QET 根据主数据和生产规则生成。 + +### 8.10 后续需要 QET 开发者确认的问题 + +如果要从当前 3D 布线走到甲方取线表,需要 QET 侧确认: + +1. `wire_style_id` 能否稳定回查导线型号、颜色、截面积。 +2. 线鼻子型号、剥皮长度、导线半脱长度来自哪里。 +3. 号码管字符当前生成规则是否已经在 QET 中存在。 +4. 始端区域、末端区域、小区域、分线束编号、总线束编号的规则由谁生成。 +5. QET 是否准备消费 FreeCAD 回写的 `routed_wires[]`。 +6. QET 最终是否负责导出甲方 Excel 取线表,还是 FreeCAD 直接导出。 + +推荐边界: + +```text +FreeCAD: + 负责 3D 设备、端子、路径、线长、碰撞状态。 + +QET: + 负责 2D 电气连接、线号、线型、颜色、截面积、线鼻子、剥皮长度、区域、取线表格式。 +``` + +## 9. 任务描述合理性修订标注 + +本章不覆盖原始任务表,而是标注其中不够合理或容易误导开发范围的描述,方便后续和项目负责人、QET 开发者、自动布线开发者对齐。 + +标注规则: + +| 标记 | 含义 | +| --- | --- | +| 【建议修改】 | 原描述容易扩大范围或职责不清,建议修改。 | +| 【建议拆分】 | 一个任务里混入多类能力,建议拆成多个阶段。 | +| 【建议降级】 | 第一版不应承诺完整能力,只做基础版或依赖 FreeCAD 原生能力。 | +| 【职责需拆分】 | 该能力不能只由 FreeCAD 完成,需要 QET 或生产数据配合。 | + +### 9.1 三期 3D 建模功能任务描述 + +| 原任务 | 合理性判断 | 建议改法 | +| --- | --- | --- | +| `3D 数据模型与映射规范开发`:基于 QET 设备、符号、端子、项目数据库,建立设备-3D资产-场景实例-端子连接点-2D图元映射关系;定义 STEP/IGES/FCStd 资产、sidecar 元数据、设备参数、安装规则、连接点语义。 | 【建议拆分】这句话把第一版最小映射、资产格式、sidecar、参数、安装规则、连接点语义都混在一起,范围过大。并且当前正式路线不建议用 `connectionPoint` 作为核心术语,也不建议第一版依赖 sidecar。 | 改为:`建立 QET 设备/端子与 FreeCAD 设备实例/工程端子的最小映射;定义 FCStd 电气资产、模板端子、工程端子和 3D 回写协议。STEP/IGES 仅作为几何输入,sidecar、设备参数、安装规则作为后续扩展。` | +| `FreeCAD 参数化设备建模能力开发`:支持断路器、继电器、端子排、导轨、线槽、柜体等常用结构生成。 | 【建议降级】这相当于做一个完整参数化设备库,工作量很大,不应和端子/布线第一版绑死。 | 改为:`第一版支持 STEP/FCStd 资产加模板端子和少量基础载体模板;断路器、继电器、端子排、导轨、线槽、柜体的完整参数化生成作为设备库后续任务。` | +| `3D 资产绑定与导入管理开发`:支持绑定 FreeCAD 生成模型或外部 STEP/IGES/STL 资产。 | 【建议修改】STL 只有网格几何,不适合作为电气语义资产;正式电气资产应优先 FCStd。 | 改为:`优先支持 FCStd 电气资产;STEP/IGES 作为几何输入;STL/OBJ 只作为显示类或临时参考资产,不作为正式可接线设备资产。` | +| `3D 资产绑定与导入管理开发`:提供版本记录、缺失诊断、重新加载、模型元数据读取能力。 | 【职责需拆分】版本记录和资产绑定主要在 QET/设备库侧;FreeCAD 侧负责读取模型语义和诊断导入状态。 | 改为:`QET 负责资产绑定、版本记录、路径解析;FreeCAD 负责导入诊断、FCStd 模板端子读取、模型变更后的场景更新提示。` | +| `复杂设备结构装配开发`:支持设备拖放、吸附、对齐、旋转、偏移、安装宿主绑定和装配约束保存。 | 【建议修改】“装配约束”容易被理解为 SW Mate 级机械约束,第一版不现实。 | 改为:`第一版依赖 FreeCAD 原生移动/旋转,补充轻量电气装配语义:安装面、安装宿主、吸附结果、局部坐标。完整机械 Mate 系统不作为第一版目标。` | +| `3D 视图导航功能开发`:实现缩放、平移、旋转、视角切换、选择高亮。 | 【建议降级】这些是 FreeCAD 原生能力,不应作为大量二开任务。 | 改为:`复用 FreeCAD 原生视图能力;二开只做对象定位、端子/导线高亮、设备预览、错误对象聚焦。` | +| `2D 到 3D 单向联动开发`:根据原理图/布置图中的设备、端子、柜体、导轨、线槽信息,单向生成或更新 3D 场景实例。 | 【职责需拆分】设备、端子、导线任务可以由 QET 当前数据提供;导轨、线槽、安装板等结构载体未必来自 2D 原理图,需要明确数据源。 | 改为:`QET 第一版导出设备、端子、导线任务、3D 资产路径和柜体上下文;FreeCAD 生成或更新设备、工程端子和导线任务。导轨、线槽、安装板可先由 FreeCAD 场景或机柜 FCStd 提供。` | +| `3D 布线基础能力开发`:基于 3D 连接点和端子映射。 | 【建议修改】当前正式术语应统一为“端子”,不要继续把 `连接点 connectionPoint` 作为核心。 | 改为:`基于 3D 工程端子和 2D terminal_uuid 映射,支持手动布线、路径点编辑、路径显示和自动布线接口。` | + +### 9.2 三维布线功能任务描述 + +| 原任务 | 合理性判断 | 建议改法 | +| --- | --- | --- | +| `三期-3 维布线功能交付`:完成 3D 自动布线全链路功能开发,实现智能连接、规则化布线、手动调整与实时错误校验。 | 【建议拆分】自动布线全链路依赖端子、装配、布线路径、QET 导线任务、电气规则和取线表,不能作为一个单点任务承诺。 | 改为:`先完成工程端子、手动布线、路径保存和线长回写;再基于稳定端子和布线路径网络开发自动布线、规则检查和生产数据输出。` | +| `布线数据模型设计`:定义端子、设备、线缆的数据结构与接口规范。 | 基本合理,但应明确 FreeCAD/QET 边界。 | 改为:`QET 定义电气连接、线号、线型、规格;FreeCAD 定义工程端子、布线路径、线长、碰撞状态和回写结构。` | +| `基础数据解析开发`:开发 3D 场景中设备、端子数据的读取与解析模块。 | 合理,但“端子数据没取对”这类问题通常不只在 FreeCAD,可能是 QET 导出的端子和 FCStd 模板槽位不匹配。 | 增加说明:`解析模块需要同时校验 QET terminal_uuid、terminal_label/slot_name_hint 与 FCStd 模板端子槽位。` | +| `智能连接识别算法开发`:实现端子与线缆、设备接口的自动匹配识别逻辑。 | 【建议降级】第一版不应做复杂智能推断,必须先以 `terminal_uuid` 精确绑定为主。 | 改为:`第一版按 terminal_uuid 精确匹配;slot_name_hint/terminal_label 只作模板槽位匹配提示;复杂智能识别作为后续。` | +| `更新BOM并生成取线表`:自动更新线长、线表和 BOM 数据,生成生产用取线表。 | 【职责需拆分】FreeCAD 只能可靠生成 3D 路径和线长;取线表中的线鼻子、剥皮长度、导线型号、颜色、区域、生产数量主要来自 QET/生产规则。 | 改为:`FreeCAD 回写 routed_wires[],包含线长、路径、起终点端子和状态;QET 根据主数据生成 BOM、线表和取线表。` | +| `电气布线规则梳理`:明确布线的电气规范,如线距、转角、分层规则等,甲方提供。 | 合理,但它是算法开发的输入,不应等到后期才补。 | 调整为前置任务:`在自动布线前由甲方确认最小规则集:线距、转角、线槽优先级、强弱电分层、线槽容量、禁止区域。` | +| `路径规划算法开发`:基于规则实现自动生成无碰撞、合规的布线路径。 | 【建议修改】“无碰撞、合规”是理想目标,不应绝对承诺。实际应生成候选路径并给出诊断。 | 改为:`基于布线路径网络生成优选路径;尽量避障并标记碰撞/间隙/越界诊断,不能保证所有复杂场景自动无碰撞。` | +| `手动调整功能开发`:实现布线路径的拖拽、修改、撤销/重做等交互功能。 | 【建议降级】完整拖拽路径点和撤销重做 UI 工作量大,第一版可以先做路径点添加/删除/重新生成,并复用 FreeCAD 原生撤销。 | 改为:`第一版支持设置起点、终点、添加路径点、删除路径点、重新生成导线;拖拽路径点作为后续体验增强。` | +| `实时错误检查逻辑开发`:开发布线冲突、连接错误、电气规范违规的实时检测。 | 【建议降级】真正实时检测成本高,第一版可以做命令执行前后检查和批量诊断。 | 改为:`第一版做布线前置检查、生成后诊断和错误高亮;实时跟随鼠标/拖拽刷新作为后续。` | +| `整体联调与验收`:全流程功能联调,修复 bug 并完成验收测试。 | 合理,但需要明确验收样例。 | 增加说明:`验收必须基于甲方 KYN28/PT2 柜样例、真实 2d_to_3d.json、真实 FCStd 设备资产和取线表样例。` | + +### 9.3 建议调整后的第一版任务口径 + +如果要让任务表更符合当前项目实际,建议第一版总目标改成: + +```text +完成 QET 与 FreeCAD 的 3D 电气设计最小闭环: + +1. QET 提供设备、端子、导线任务和 3D 资产路径。 +2. FreeCAD 导入 FCStd 电气资产和柜体场景。 +3. FreeCAD 从模板端子生成工程端子。 +4. 用户可手动连接两个工程端子并编辑路径。 +5. FreeCAD 保存 scene.FCStd,并回写设备实例、端子绑定、已布导线、路径长度和诊断状态。 +6. QET 根据回写结果和电气主数据生成取线表。 +``` + +第一版不应承诺: + +- 完整 SW Mate 级装配系统。 +- 完整参数化设备库。 +- 完整自动端子排生成。 +- 完整 EPLAN 级自动布线规则库。 +- FreeCAD 单独生成最终生产取线表。 + +这些可以作为第二阶段或第三阶段能力继续扩展。 diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index f203606..204c93d 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -7,6 +7,9 @@ import json import itertools import math +import os +import re +import sqlite3 import FreeCAD as App @@ -21,6 +24,10 @@ import TemplateSemantics import WiringObjects +AUTO_ROUTING_RUNTIME_VERSION = "2026-06-08-runtime-routing-v4" +LOCAL_ACCESS_DETOUR_CLEARANCE = 10.0 + + DEFAULT_OPTIONS = { # 端子出来先走一小段,避免导线贴着设备外壳起步。 "terminal_exit_length": 20.0, @@ -28,12 +35,37 @@ DEFAULT_OPTIONS = { "lane_spacing": 10.0, "lane_max_offset": 30.0, "segment_reuse_penalty": 200.0, + # SW/EPLAN 风格路径约束的第一步:可按 carrier 名称/标签/源标签/类型禁止经过。 + "forbidden_route_carrier_names": [], + "forbidden_route_carrier_labels": [], + "forbidden_route_carrier_source_labels": [], + "forbidden_route_carrier_kinds": [], + "required_route_carrier_names": [], + "required_route_carrier_labels": [], + "required_route_carrier_source_labels": [], + "required_route_carrier_kinds": [], # 线槽网络相关参数。 "use_routing_network": True, "network_entry_max_distance": 1000.0, "network_entry_candidate_limit": 8, + # 批量布线默认收窄入口候选,避免真实工程里 入口候选 x 出口候选 x 导线数量 过度放大。 + # 设为 0 可关闭批量收窄,继续使用 network_entry_candidate_limit。 + "batch_network_entry_candidate_limit": 3, + # 批量模式下,柜内候选/无碰撞候选最多把每侧入口扩到这个总数。 + # 单根布线不使用该默认值;缺路径重试会按重试候选数临时放宽。 + "batch_network_entry_total_candidate_limit": 6, + # 单根线因候选裁剪过窄不可达时,用更大的候选数补救一次,避免全量批量都退回慢路径。 + "missing_route_retry_candidate_limit": 8, + # 第一版批量布线优先保证真实工程能完成;路径仍会做碰撞诊断,必要时可手动开启障碍过滤求路。 + "batch_avoid_obstacles": False, + # 只对已经发生第三方设备/布局碰撞的少量导线做二次避障,避免全量开启避障拖慢真实工程。 + "selective_collision_reroute": True, + "selective_collision_reroute_limit": 5, + "selective_collision_reroute_allow_fallback": False, "network_entry_distance_cost_factor": 5.0, + "terminal_access_warning_distance": 0.0, "route_candidate_collision_penalty": 10000.0, + "route_candidate_boundary_penalty": 100000.0, "ignore_endpoint_near_obstacles": True, "adjoining_duct_tolerance": RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, "bend_penalty": 25.0, @@ -51,8 +83,14 @@ DEFAULT_OPTIONS = { # 主干必须走 carrier/贴面网络;没有布线路径网络时直接失败。 # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 "obstacle_clearance": 5.0, + "local_access_obstacle_scan_margin": 0.0, # 端子出线/入线段通常会贴近端子塑壳或设备外壳,不作为主路径碰撞判定依据。 "ignore_endpoint_collision_segments": True, + # 即使批量布线不启用全局避障,也让线槽/主路径到端子的局部接入折线绕开第三方设备。 + "avoid_local_access_obstacles": True, + # 导入柜体/门板/支架等结构件通常没有 QET 设备绑定;第一版把未绑定结构件视为可穿越结构, + # 不把它们计入导线碰撞,避免线槽内导线被柜体 AABB 误报淹没。带 element_uuid 的设备碰撞仍保留。 + "auto_ignore_unbound_structural_obstacles": True, # 防止坐标异常或端子离路由网络过远时生成超长接入线,把 FreeCAD # 视图包围盒拉得过大,导致旋转时模型被裁剪到看不见。 "terminal_access_max_distance": 1000.0, @@ -61,6 +99,14 @@ DEFAULT_OPTIONS = { "avoid_obstacles": True, "replace_existing": True, "hide_route_carriers_after_route": True, + "wire_style_database_path": "", + "preflight_routeability_sample_limit": 0, + # 自动布线时如果诊断能明确建议“线槽组件 -> 端子主网络”的桥接点, + # 先生成 UserPath 桥再布线,避免真实工程长期退回 RoutingRange 兜底。 + "auto_create_diagnostic_bridges": False, + # 第一次布线若发现“兜底区域 -> 当前主路径”的缺主路径绕行配对, + # 自动补一段 UserPath 桥并重跑一次,让少量剩余碰撞线回到主路径网络。 + "auto_create_main_path_detour_bridges": False, } @@ -75,6 +121,117 @@ def _merged_options(options): return merged +def _has_route_constraints(options): + opts = options or {} + for key in ( + "forbidden_route_carrier_names", + "forbidden_route_carrier_labels", + "forbidden_route_carrier_source_names", + "forbidden_route_carrier_source_labels", + "forbidden_route_carrier_kinds", + "required_route_carrier_names", + "required_route_carrier_labels", + "required_route_carrier_source_names", + "required_route_carrier_source_labels", + "required_route_carrier_kinds", + ): + value = opts.get(key) + if isinstance(value, (list, tuple, set)) and any(str(item or "").strip() for item in value): + return True + if isinstance(value, str) and value.strip(): + return True + return False + + +def _option_text_list(options, key): + value = (options or {}).get(key) + if isinstance(value, str): + values = [value] + elif isinstance(value, (list, tuple, set)): + values = list(value) + else: + values = [] + result = [] + seen = set() + for item in values: + text = str(item or "").strip() + if not text or text in seen: + continue + seen.add(text) + result.append(text) + return result + + +def _route_constraint_payload(options): + groups = { + "required": { + "names": _option_text_list(options, "required_route_carrier_names"), + "labels": _option_text_list(options, "required_route_carrier_labels"), + "source_names": _option_text_list(options, "required_route_carrier_source_names"), + "source_labels": _option_text_list(options, "required_route_carrier_source_labels"), + "kinds": _option_text_list(options, "required_route_carrier_kinds"), + }, + "forbidden": { + "names": _option_text_list(options, "forbidden_route_carrier_names"), + "labels": _option_text_list(options, "forbidden_route_carrier_labels"), + "source_names": _option_text_list(options, "forbidden_route_carrier_source_names"), + "source_labels": _option_text_list(options, "forbidden_route_carrier_source_labels"), + "kinds": _option_text_list(options, "forbidden_route_carrier_kinds"), + }, + } + return { + group: { + key: values + for key, values in payload.items() + if values + } + for group, payload in groups.items() + if any(payload.values()) + } + + +_ROUTE_CONSTRAINT_OPTION_KEYS = ( + "forbidden_route_carrier_names", + "forbidden_route_carrier_labels", + "forbidden_route_carrier_source_names", + "forbidden_route_carrier_source_labels", + "forbidden_route_carrier_kinds", + "required_route_carrier_names", + "required_route_carrier_labels", + "required_route_carrier_source_names", + "required_route_carrier_source_labels", + "required_route_carrier_kinds", +) + + +def _merge_route_constraint_options(options, extra_options): + merged = dict(options or {}) + for key in _ROUTE_CONSTRAINT_OPTION_KEYS: + values = [] + seen = set() + for source in (options, extra_options): + for item in _option_text_list(source or {}, key): + if item in seen: + continue + seen.add(item) + values.append(item) + if values: + merged[key] = values + return merged + + +def _document_route_constraint_options(doc): + collector = getattr(RoutingNetwork, "collect_route_constraint_options", None) + if not callable(collector): + # 运行目录模块偶尔会和 AutoRouting 版本不一致;缺少约束收集函数时退回“无文档级约束”。 + return {} + try: + result = collector(doc) + except Exception: + return {} + return result if isinstance(result, dict) else {} + + def _vector(point): if isinstance(point, App.Vector): return App.Vector(point.x, point.y, point.z) @@ -257,10 +414,202 @@ def _orthogonal_hit_count(points, obstacle_bboxes): return hits +def _segment_hit_bboxes(points, obstacle_bboxes): + hit_bboxes = [] + seen = set() + if not obstacle_bboxes: + return hit_bboxes + for index in range(max(len(points or []) - 1, 0)): + start = points[index] + end = points[index + 1] + for bbox in obstacle_bboxes: + if not _segment_intersects_bbox(start, end, bbox): + continue + identity = id(bbox) + if identity in seen: + continue + seen.add(identity) + hit_bboxes.append(bbox) + return hit_bboxes + + +def _bbox_interval_overlap(first_min, first_max, second_min, second_max): + return float(first_min) <= float(second_max) and float(second_min) <= float(first_max) + + +def _bbox_overlaps_segment_envelope(start_point, end_point, bbox, margin=0.0): + if bbox is None: + return False + margin = max(float(margin or 0.0), 0.0) + try: + return ( + _bbox_interval_overlap( + min(float(start_point.x), float(end_point.x)) - margin, + max(float(start_point.x), float(end_point.x)) + margin, + float(bbox["xmin"]), + float(bbox["xmax"]), + ) + and _bbox_interval_overlap( + min(float(start_point.y), float(end_point.y)) - margin, + max(float(start_point.y), float(end_point.y)) + margin, + float(bbox["ymin"]), + float(bbox["ymax"]), + ) + and _bbox_interval_overlap( + min(float(start_point.z), float(end_point.z)) - margin, + max(float(start_point.z), float(end_point.z)) + margin, + float(bbox["zmin"]), + float(bbox["zmax"]), + ) + ) + except Exception: + return True + + +def _filter_obstacle_bboxes_near_polyline(points, obstacle_bboxes, margin=0.0): + if not obstacle_bboxes: + return [] + if len(points or []) < 2: + return list(obstacle_bboxes or []) + filtered = [] + seen = set() + for bbox in obstacle_bboxes or []: + for index in range(max(len(points or []) - 1, 0)): + if not _bbox_overlaps_segment_envelope(points[index], points[index + 1], bbox, margin=margin): + continue + identity = id(bbox) + if identity not in seen: + seen.add(identity) + filtered.append(bbox) + break + return filtered + + +def _local_access_obstacle_bboxes(start_point, end_point, obstacle_bboxes, preferred_axis=None, margin=LOCAL_ACCESS_DETOUR_CLEARANCE): + if not obstacle_bboxes: + return [] + # 局部接入段只需要考虑贴近自身正交基线路径的障碍;远处设备留给最终碰撞诊断, + # 避免真实机柜中“端子接入 x 全部模型”导致批量布线耗时爆炸。 + base_points = _orthogonal_points(start_point, end_point, preferred_axis) + return _filter_obstacle_bboxes_near_polyline(base_points, obstacle_bboxes, margin=margin) + + +def _is_local_access_obstacle(obstacle): + if not isinstance(obstacle, dict): + return False + if ( + str(obstacle.get("element_uuid", "") or "").strip() + or str(obstacle.get("instance_id", "") or "").strip() + ): + return True + parent_refs = obstacle.get("parent_refs", {}) if isinstance(obstacle.get("parent_refs", {}), dict) else {} + own_text = " ".join( + str(part or "").lower() + for part in [ + obstacle.get("label", ""), + obstacle.get("name", ""), + ] + ) + if any(keyword in own_text for keyword in _DEVICE_COLLISION_KEYWORDS): + return True + text_parts = [ + obstacle.get("label", ""), + obstacle.get("name", ""), + ] + text_parts.extend(list(parent_refs.get("labels", []) or [])) + text_parts.extend(list(parent_refs.get("names", []) or [])) + text = " ".join(str(part or "").lower() for part in text_parts) + structural_import_markers = ( + "imported", + "qet exchange devices", + "qetexchangedevices", + "qetcabinet", + "linkgroup", + "compound", + "nauo", + ) + # 未绑定导入机械件仍会进入最终碰撞诊断,但不参与端子局部绕行; + # 否则柜体、铰链、螺丝等大量 AABB 会把每条线的接入计算放大到不可接受。 + if any(marker in text for marker in structural_import_markers): + return False + return True + + +def _axis_detour_values(axis, hit_bboxes, clearance=LOCAL_ACCESS_DETOUR_CLEARANCE): + min_key = "{0}min".format(axis) + max_key = "{0}max".format(axis) + values = [] + for bbox in hit_bboxes or []: + try: + values.append(float(bbox[min_key]) - float(clearance)) + values.append(float(bbox[max_key]) + float(clearance)) + except Exception: + continue + return values + + +def _orthogonal_detour_points(start_point, end_point, axis_order, detour_axis, detour_value): + points = [start_point] + current = start_point + if abs(_axis_value(current, detour_axis) - float(detour_value)) > 0.000001: + current = _with_axis(current, detour_axis, float(detour_value)) + _append_unique(points, current) + for axis in axis_order: + if axis == detour_axis: + continue + target = _axis_value(end_point, axis) + if abs(_axis_value(current, axis) - target) <= 0.000001: + continue + current = _with_axis(current, axis, target) + _append_unique(points, current) + end_axis_value = _axis_value(end_point, detour_axis) + if abs(_axis_value(current, detour_axis) - end_axis_value) > 0.000001: + current = _with_axis(current, detour_axis, end_axis_value) + _append_unique(points, current) + _append_unique(points, end_point) + return points + + +def _orthogonal_points_with_local_detour(start_point, end_point, obstacle_bboxes, axis_order, best_points, best_hits): + hit_bboxes = _segment_hit_bboxes(best_points, obstacle_bboxes) + if not hit_bboxes: + return best_points + + active_axes = { + axis + for axis in ("x", "y", "z") + if abs(_axis_value(end_point, axis) - _axis_value(start_point, axis)) > 0.000001 + } + detour_axes = [axis for axis in ("x", "y", "z") if axis not in active_axes] + if not detour_axes: + detour_axes = [axis for axis in ("x", "y", "z")] + + # 设备端子到主路径的接入段常是一条很长的直线;只重排轴顺序时绕不过 + # 同轴障碍盒。这里增加一个局部侧向绕行候选,成本低,且不改变主路径网络。 + chosen_points = best_points + chosen_hits = best_hits + for detour_axis in detour_axes: + for value in _axis_detour_values(detour_axis, hit_bboxes): + candidate_points = _orthogonal_detour_points( + start_point, + end_point, + axis_order, + detour_axis, + value, + ) + candidate_hits = _orthogonal_hit_count(candidate_points, obstacle_bboxes) + if candidate_hits < chosen_hits: + chosen_hits = candidate_hits + chosen_points = candidate_points + if chosen_hits <= 0: + return chosen_points + return chosen_points + + def _orthogonal_points_avoiding_obstacles(start_point, end_point, obstacle_bboxes, preferred_axis=None): base_order = _orthogonal_axis_order(start_point, end_point, preferred_axis) base_points = _orthogonal_points_for_axis_order(start_point, end_point, base_order) - if not obstacle_bboxes or len(base_order) <= 1: + if not obstacle_bboxes: return base_points active_axes = [ @@ -269,7 +618,17 @@ def _orthogonal_points_avoiding_obstacles(start_point, end_point, obstacle_bboxe if abs(_axis_value(end_point, axis) - _axis_value(start_point, axis)) > 0.000001 ] if len(active_axes) <= 1: - return base_points + base_hits = _orthogonal_hit_count(base_points, obstacle_bboxes) + if base_hits <= 0: + return base_points + return _orthogonal_points_with_local_detour( + start_point, + end_point, + obstacle_bboxes, + base_order, + base_points, + base_hits, + ) inactive_axes = [axis for axis in base_order if axis not in active_axes] best_points = base_points @@ -294,6 +653,15 @@ def _orthogonal_points_avoiding_obstacles(start_point, end_point, obstacle_bboxe best_points = candidate_points if best_hits <= 0: break + if best_hits > 0: + best_points = _orthogonal_points_with_local_detour( + start_point, + end_point, + obstacle_bboxes, + base_order, + best_points, + best_hits, + ) return best_points @@ -445,6 +813,113 @@ def index_terminals(doc): return indexed +def _terminal_element_summary(terminals, element_uuid, limit=5): + return _terminal_property_summary(terminals, "QetElementUuid", element_uuid, limit=limit) + + +def _terminal_instance_summary(terminals, instance_id, limit=5): + return _terminal_property_summary(terminals, "QetInstanceId", instance_id, limit=limit) + + +def _terminal_property_summary(terminals, property_name, expected_value, limit=5): + expected = str(expected_value or "").strip() + if not expected: + return {"count": 0, "samples": []} + samples = [] + count = 0 + for terminal_uuid, terminal in (terminals or {}).items(): + terminal_value = str(getattr(terminal, property_name, "") or "").strip() + if terminal_value != expected: + continue + count += 1 + if len(samples) >= limit: + continue + samples.append( + { + "terminal_uuid": str(terminal_uuid or "").strip(), + "label": str(getattr(terminal, "Label", "") or "").strip(), + "name": str(getattr(terminal, "Name", "") or "").strip(), + "terminal_label": str(getattr(terminal, "QetTerminalLabel", "") or "").strip(), + "instance_id": str(getattr(terminal, "QetInstanceId", "") or "").strip(), + } + ) + return {"count": count, "samples": samples} + + +_MISSING_ENDPOINT_REASON_LABELS = { + "missing_terminal_uuid": "导线端点缺少 terminal_uuid", + "missing_device_binding_metadata": "导线端点缺少 2D/3D 设备绑定信息", + "device_not_in_3d_scene": "该 2D 设备未在 FreeCAD 场景中找到", + "no_3d_terminals_for_element": "该 2D 设备在 FreeCAD 中没有工程端子", + "no_3d_terminals_for_instance": "该 3D 实例在 FreeCAD 中没有工程端子", + "terminal_uuid_not_in_element": "同设备存在端子,但没有匹配该 terminal_uuid", +} + + +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" + 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: + return "missing_device_binding_metadata" + if sample.get("{0}_device_in_scene".format(side)) is False: + return "device_not_in_3d_scene" + element_count = _safe_int(sample.get("{0}_element_terminal_count".format(side), 0)) + instance_count = _safe_int(sample.get("{0}_instance_terminal_count".format(side), 0)) + if element_count <= 0: + return "no_3d_terminals_for_element" + if instance_count <= 0 and str(sample.get("{0}_instance_id".format(side), "") or "").strip(): + return "no_3d_terminals_for_instance" + return "terminal_uuid_not_in_element" + + +def _endpoint_device_summary(doc, instance_id, element_uuid): + device_group = None + if doc is not None: + device_group = TerminalObjects.find_device_group_by_instance_id(doc, instance_id) + if device_group is None: + device_group = TerminalObjects.find_device_group(doc, element_uuid) + if device_group is None: + return {"in_scene": False, "name": "", "label": ""} + return { + "in_scene": True, + "name": str(getattr(device_group, "Name", "") or "").strip(), + "label": str(getattr(device_group, "Label", "") or "").strip(), + } + + +def _add_missing_endpoint_terminal_context(sample, side, terminals, doc=None): + if not isinstance(sample, dict): + return sample + device_summary = _endpoint_device_summary( + doc, + sample.get("{0}_instance_id".format(side), ""), + sample.get("{0}_element_uuid".format(side), ""), + ) + sample["{0}_device_in_scene".format(side)] = bool(device_summary.get("in_scene", False)) + existing_name = str(sample.get("{0}_device_name".format(side), "") or "").strip() + existing_label = str(sample.get("{0}_device_label".format(side), "") or "").strip() + sample["{0}_device_name".format(side)] = str(device_summary.get("name", "") or "").strip() or existing_name + sample["{0}_device_label".format(side)] = str(device_summary.get("label", "") or "").strip() or existing_label + element_uuid = sample.get("{0}_element_uuid".format(side), "") + element_summary = _terminal_element_summary(terminals, element_uuid) + sample["{0}_element_terminal_count".format(side)] = int(element_summary.get("count", 0) or 0) + sample["{0}_element_terminal_samples".format(side)] = list(element_summary.get("samples", []) or []) + instance_id = sample.get("{0}_instance_id".format(side), "") + instance_summary = _terminal_instance_summary(terminals, instance_id) + sample["{0}_instance_terminal_count".format(side)] = int(instance_summary.get("count", 0) or 0) + sample["{0}_instance_terminal_samples".format(side)] = list(instance_summary.get("samples", []) or []) + reason_code = _missing_endpoint_reason_code(sample, side) + sample["{0}_missing_endpoint_reason_code".format(side)] = reason_code + sample["{0}_missing_endpoint_reason_label".format(side)] = _MISSING_ENDPOINT_REASON_LABELS.get( + reason_code, + reason_code, + ) + return sample + + def _normalized_match_token(value): return (value or "").strip().lower().replace(" ", "") @@ -735,6 +1210,10 @@ def _set_string(obj, name, value, description="Routing connection property"): TerminalObjects.ensure_string_property(obj, name, "QET Routing", description, value) +def _set_bool(obj, name, value, description="Routing connection property"): + TerminalObjects.ensure_bool_property(obj, name, "QET Routing", description, value) + + def _clean_endpoint_metadata(endpoint_metadata): if not isinstance(endpoint_metadata, dict): return {} @@ -772,28 +1251,320 @@ def _set_endpoint_metadata(wire, endpoint_metadata): return metadata -def _route_payload(route_data, collisions, wire_style_id="", endpoint_metadata=None): +def _clean_wire_style_payload(wire_style): + if not isinstance(wire_style, dict): + return {} + allowed = ( + "id", + "project_uuid", + "name", + "line_color", + "line_type", + "line_width", + "diameter_mm", + "area_or_spec", + "wire_type", + "wire_format", + "description", + ) + payload = {} + for key in allowed: + value = wire_style.get(key) + if value is None: + continue + if isinstance(value, str): + value = value.strip() + if not value: + continue + payload[key] = value + return payload + + +def _wire_style_status(wire_style_id, wire_style): + style_id = str(wire_style_id or "").strip() + if not style_id: + return "" + return "Resolved" if _clean_wire_style_payload(wire_style) else "Missing" + + +def _route_boundary_payload(route_data): + network = route_data.get("network", {}) if isinstance(route_data, dict) else {} + if not isinstance(network, dict): + network = {} + boundary_aware = bool(network.get("boundary_aware", False)) + try: + violations = int(network.get("route_candidate_boundary_violations", 0) or 0) + except Exception: + violations = 0 + status = "" + if boundary_aware: + status = "BoundaryWarning" if violations > 0 else "InsideBoundary" + return { + "boundary_aware": boundary_aware, + "boundary_violation_count": max(violations, 0), + "boundary_status": status, + } + + +def _route_lane_capacity_payload(route_data): + lane = route_data.get("lane", {}) if isinstance(route_data, dict) else {} + if not isinstance(lane, dict): + lane = {} + try: + lane_index = max(int(lane.get("index", 0) or 0), 0) + except Exception: + lane_index = 0 + try: + lane_offset = float(lane.get("offset_mm", 0.0) or 0.0) + except Exception: + lane_offset = 0.0 + try: + lane_spacing = float(lane.get("spacing_mm", 0.0) or 0.0) + except Exception: + lane_spacing = 0.0 + try: + min_capacity = _route_track_min_capacity(route_data.get("route_track", {})) + except Exception: + min_capacity = None + parallel_wire_count = lane_index + 1 + capacity_status = "" + if min_capacity is not None: + capacity_status = "CapacityWarning" if parallel_wire_count > min_capacity else "WithinCapacity" + return { + "lane_index": lane_index, + "lane_axis": str(lane.get("axis", "") or "").strip(), + "lane_offset_mm": lane_offset, + "lane_spacing_mm": lane_spacing, + "parallel_wire_count": parallel_wire_count, + "min_carrier_capacity": min_capacity, + "capacity_status": capacity_status, + } + + +def _route_access_payload(route_data): + network = route_data.get("network", {}) if isinstance(route_data, dict) else {} + if not isinstance(network, dict): + network = {} + + def float_value(key, default=0.0): + try: + return float(network.get(key, default) or default) + except Exception: + return float(default) + + def int_value(key, default=1): + try: + return int(network.get(key, default) or default) + except Exception: + return int(default) + + entry_distance = float_value("entry_distance") + exit_distance = float_value("exit_distance") + warning_distance = float_value("terminal_access_warning_distance") + warning_sides = [] + if warning_distance > 0.0: + if entry_distance > warning_distance: + warning_sides.append("entry") + if exit_distance > warning_distance: + warning_sides.append("exit") + access_status = "LongAccessWarning" if warning_sides else "NormalAccess" + return { + "entry_distance_mm": entry_distance, + "exit_distance_mm": exit_distance, + "entry_point_mode": str(network.get("entry_point_mode", "") or "").strip(), + "exit_point_mode": str(network.get("exit_point_mode", "") or "").strip(), + "entry_candidate_rank": int_value("entry_candidate_rank"), + "exit_candidate_rank": int_value("exit_candidate_rank"), + "access_warning_distance_mm": warning_distance, + "access_status": access_status, + "warning_sides": warning_sides, + } + + +def _route_collision_payload(collisions): + hard_count = 0 + clearance_count = 0 + for collision in collisions or []: + if not isinstance(collision, dict): + hard_count += 1 + continue + kind = str(collision.get("collision_kind", "") or "").strip() + if kind == "ClearanceWarning": + clearance_count += 1 + else: + hard_count += 1 + collision_count = hard_count + clearance_count + if hard_count > 0: + status = "HardIntersectionWarning" + elif clearance_count > 0: + status = "ClearanceWarning" + else: + status = "NoCollision" + return { + "collision_count": collision_count, + "hard_intersection_count": hard_count, + "clearance_warning_count": clearance_count, + "collision_status": status, + } + + +def _route_quality_payload(route_track): + carrier_kinds = _route_track_carrier_kinds(route_track) + fallback_kinds = [ + kind + for kind in ("RoutingRange", "AuxiliaryPath") + if carrier_kinds.get(kind, 0) + ] + fallback_labels = _route_warning_carrier_labels(route_track, fallback_kinds, limit=8) + return { + "quality_status": "FallbackPathWarning" if fallback_kinds else "NormalPath", + "fallback_carrier_kinds": fallback_kinds, + "fallback_carrier_labels": fallback_labels, + } + + +def _route_issue_codes(route_data, collisions): + route_track = route_data.get("route_track", {}) if isinstance(route_data, dict) else {} + codes = [] + + def append_once(code, enabled=True): + if not enabled or code in codes: + return + codes.append(code) + + append_once( + "long_terminal_access", + _route_access_payload(route_data).get("access_status") == "LongAccessWarning", + ) + append_once( + "collision_warnings", + _route_collision_payload(collisions).get("collision_count", 0) > 0, + ) + relation_counts = {} + for collision in list(collisions or []): + if not isinstance(collision, dict): + continue + relation = str(collision.get("collision_relation", "") or "").strip() + if relation: + relation_counts[relation] = relation_counts.get(relation, 0) + 1 + append_once( + "third_party_device_collisions", + _safe_int(relation_counts.get("third_party_device_collision", 0)) > 0, + ) + append_once( + "endpoint_device_collisions", + _safe_int(relation_counts.get("endpoint_device_collision", 0)) > 0, + ) + append_once( + "route_quality_warnings", + _route_quality_payload(route_track).get("quality_status") == "FallbackPathWarning", + ) + append_once( + "route_capacity_pressure", + _route_lane_capacity_payload(route_data).get("capacity_status") == "CapacityWarning", + ) + append_once( + "route_candidate_boundary_violations", + _route_boundary_payload(route_data).get("boundary_status") == "BoundaryWarning", + ) + append_once( + "main_path_detour_missing", + str(route_data.get("selective_collision_reroute_status", "") or "").strip() + == "RejectedFallback", + ) + network = route_data.get("network", {}) if isinstance(route_data, dict) else {} + if isinstance(network, dict): + try: + obstacle_hits = int(network.get("route_candidate_obstacle_hits", 0) or 0) + except Exception: + obstacle_hits = 0 + append_once("route_candidate_obstacle_hits", obstacle_hits > 0) + return codes + + +def _route_payload(route_data, collisions, wire_style_id="", endpoint_metadata=None, wire_style=None): points = route_data.get("points", []) + style_status = _wire_style_status(wire_style_id, wire_style) + boundary_payload = _route_boundary_payload(route_data) + lane_capacity_payload = _route_lane_capacity_payload(route_data) + access_payload = _route_access_payload(route_data) + collision_payload = _route_collision_payload(collisions) + route_track = route_data.get("route_track", {}) + quality_payload = _route_quality_payload(route_track) + issue_codes = _route_issue_codes(route_data, collisions) payload = { "algorithm": route_data.get("algorithm", ""), "length_mm": _route_length(points), "wire_style_id": str(wire_style_id or "").strip(), "lane": route_data.get("lane", {}), "points": [_point_payload(point) for point in points], - "collision_count": len(collisions), + "collision_count": collision_payload["collision_count"], "collisions": collisions, + "collision_summary": collision_payload, "network": route_data.get("network", {}), - "route_track": route_data.get("route_track", {}), + "route_track": route_track, + "route_source_labels": _route_source_labels(route_track, limit=8), + "route_carrier_names": _route_track_carrier_names(route_track, limit=8), + "issue_codes": issue_codes, + "issue_labels": [ + _routing_diagnostic_issue_label(code) + for code in issue_codes + ], } + if route_data.get("endpoint_access"): + payload["endpoint_access"] = route_data.get("endpoint_access", {}) + payload["quality"] = quality_payload + if boundary_payload["boundary_aware"]: + payload["boundary"] = boundary_payload + if lane_capacity_payload["capacity_status"]: + payload["capacity"] = lane_capacity_payload + payload["access"] = access_payload + if style_status: + payload["wire_style_status"] = style_status + selective_status = str(route_data.get("selective_collision_reroute_status", "") or "").strip() + if selective_status: + selective_payload = { + "status": selective_status, + "rejected_fallback_kinds": list( + route_data.get("selective_collision_reroute_rejected_fallback_kinds", []) or [] + ), + "rejected_fallback_labels": list( + route_data.get("selective_collision_reroute_rejected_fallback_labels", []) or [] + ), + } + payload["selective_collision_reroute"] = { + key: value + for key, value in selective_payload.items() + if value + } metadata = _clean_endpoint_metadata(endpoint_metadata) if metadata: payload["endpoint_metadata"] = metadata + style_payload = _clean_wire_style_payload(wire_style) + if style_payload: + payload["wire_style"] = style_payload return payload -def _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id="", endpoint_metadata=None): +def _set_routing_connection_metadata( + wire, + route_data, + collisions, + wire_style_id="", + endpoint_metadata=None, + wire_style=None, +): length_mm = _route_length(route_data.get("points", [])) cleaned_endpoint_metadata = _set_endpoint_metadata(wire, endpoint_metadata) + cleaned_wire_style = _clean_wire_style_payload(wire_style) + style_status = _wire_style_status(wire_style_id, cleaned_wire_style) + boundary_payload = _route_boundary_payload(route_data) + lane_capacity_payload = _route_lane_capacity_payload(route_data) + access_payload = _route_access_payload(route_data) + collision_payload = _route_collision_payload(collisions) + route_track = route_data.get("route_track", {}) + quality_payload = _route_quality_payload(route_track) + issue_codes = _route_issue_codes(route_data, collisions) _set_string( wire, "QetRouteAlgorithm", @@ -814,42 +1585,258 @@ def _set_routing_connection_metadata(wire, route_data, collisions, wire_style_id ) _set_string( wire, - "QetRouteDiagnosticsJson", - json.dumps( - _route_payload( - route_data, - collisions, - wire_style_id=wire_style_id, - endpoint_metadata=cleaned_endpoint_metadata, - ), - ensure_ascii=False, - ), - "Routing connection diagnostics", + "QetRouteIssueCodes", + _diagnostic_issue_codes_text(issue_codes), + "Routing issue codes for this wire", ) - if route_data.get("network"): - _set_string( - wire, - "QetRouteNetworkJson", - json.dumps(route_data.get("network", {}), ensure_ascii=False), - "Route network metadata used by this wire", - ) - if route_data.get("route_track"): - _set_string( - wire, - "QetRouteTrackJson", - json.dumps(route_data.get("route_track", {}), ensure_ascii=False), - "Routing carriers passed through by this wire", - ) - - -def build_network_route(start_terminal, end_terminal, route_index=0, options=None, doc=None): - opts = _merged_options(options) - if not opts.get("use_routing_network", True): - return None + _set_string( + wire, + "QetRouteIssueLabels", + _diagnostic_issue_labels_text(issue_codes), + "Routing issue labels for this wire", + ) + _set_string( + wire, + "QetRouteCollisionCount", + str(collision_payload["collision_count"]), + "Total route collision warning count", + ) + _set_string( + wire, + "QetRouteHardIntersectionCount", + str(collision_payload["hard_intersection_count"]), + "Hard route intersection count", + ) + _set_string( + wire, + "QetRouteClearanceWarningCount", + str(collision_payload["clearance_warning_count"]), + "Route clearance warning count", + ) + _set_string( + wire, + "QetRouteCollisionStatus", + collision_payload["collision_status"], + "Route collision status for this wire", + ) + _set_string( + wire, + "QetRouteQualityStatus", + quality_payload["quality_status"], + "Route quality status for this wire", + ) + _set_string( + wire, + "QetRouteFallbackCarrierKinds", + ",".join(quality_payload["fallback_carrier_kinds"]), + "Fallback route carrier kinds used by this wire", + ) + _set_string( + wire, + "QetRouteFallbackCarrierLabels", + "、".join(quality_payload["fallback_carrier_labels"]), + "Fallback route carrier labels used by this wire", + ) + _set_string( + wire, + "QetRouteLaneIndex", + str(lane_capacity_payload["lane_index"]), + "Shared route lane index", + ) + _set_string( + wire, + "QetRouteLaneAxis", + lane_capacity_payload["lane_axis"], + "Shared route lane offset axis", + ) + _set_string( + wire, + "QetRouteLaneOffsetMm", + "{0:.3f}".format(lane_capacity_payload["lane_offset_mm"]), + "Shared route lane visual offset in millimeters", + ) + _set_string( + wire, + "QetRouteLaneSpacingMm", + "{0:.3f}".format(lane_capacity_payload["lane_spacing_mm"]), + "Shared route lane spacing in millimeters", + ) + _set_string( + wire, + "QetRouteParallelWireCount", + str(lane_capacity_payload["parallel_wire_count"]), + "Parallel wire count implied by this lane", + ) + if lane_capacity_payload["min_carrier_capacity"] is not None: + _set_string( + wire, + "QetRouteMinCarrierCapacity", + str(lane_capacity_payload["min_carrier_capacity"]), + "Minimum route carrier capacity used by this wire", + ) + if lane_capacity_payload["capacity_status"]: + _set_string( + wire, + "QetRouteCapacityStatus", + lane_capacity_payload["capacity_status"], + "Route capacity status for this wire", + ) + _set_string( + wire, + "QetRouteEntryDistanceMm", + "{0:.3f}".format(access_payload["entry_distance_mm"]), + "Distance from start terminal access point to route network in millimeters", + ) + _set_string( + wire, + "QetRouteExitDistanceMm", + "{0:.3f}".format(access_payload["exit_distance_mm"]), + "Distance from route network to end terminal access point in millimeters", + ) + _set_string( + wire, + "QetRouteEntryPointMode", + access_payload["entry_point_mode"], + "How the start terminal connects to the route network", + ) + _set_string( + wire, + "QetRouteExitPointMode", + access_payload["exit_point_mode"], + "How the end terminal connects to the route network", + ) + _set_string( + wire, + "QetRouteEntryCandidateRank", + str(access_payload["entry_candidate_rank"]), + "Start terminal route-network candidate rank", + ) + _set_string( + wire, + "QetRouteExitCandidateRank", + str(access_payload["exit_candidate_rank"]), + "End terminal route-network candidate rank", + ) + _set_string( + wire, + "QetRouteAccessWarningDistanceMm", + "{0:.3f}".format(access_payload["access_warning_distance_mm"]), + "Terminal access distance warning threshold in millimeters", + ) + _set_string( + wire, + "QetRouteAccessStatus", + access_payload["access_status"], + "Terminal access distance status for this routed wire", + ) + _set_string( + wire, + "QetRouteAccessWarningSides", + ",".join(access_payload["warning_sides"]), + "Terminal sides whose route-network access distance exceeds the warning threshold", + ) + _set_string( + wire, + "QetRouteSourceLabels", + "、".join(_route_source_labels(route_track, limit=8)), + "Route source labels passed through by this wire", + ) + _set_string( + wire, + "QetRouteCarrierNames", + "、".join(_route_track_carrier_names(route_track, limit=8)), + "Route carrier object names passed through by this wire", + ) + if boundary_payload["boundary_aware"]: + _set_bool( + wire, + "QetRouteBoundaryAware", + True, + "Whether cabinet boundary scoring participated in this route", + ) + _set_string( + wire, + "QetRouteBoundaryStatus", + boundary_payload["boundary_status"], + "Cabinet boundary status for this routed wire", + ) + _set_string( + wire, + "QetRouteBoundaryViolationCount", + str(boundary_payload["boundary_violation_count"]), + "Number of routed points outside cabinet interior boundary", + ) + if style_status: + _set_string( + wire, + "QetWireStyleStatus", + style_status, + "QET wire style lookup status", + ) + if cleaned_wire_style: + _set_string( + wire, + "QetWireStyleJson", + json.dumps(cleaned_wire_style, ensure_ascii=False), + "QET wire style resolved from wire_properties", + ) + # 常用样式字段展开到对象属性,手动测试时不用每次打开 JSON。 + for key, prop_name, description in ( + ("name", "QetWireStyleName", "QET wire style name"), + ("area_or_spec", "QetWireSpecText", "QET wire specification text"), + ("line_color", "QetWireColorText", "QET wire color text"), + ("line_type", "QetWireLineType", "QET wire line type"), + ("wire_type", "QetWireType", "QET wire type"), + ("wire_format", "QetWireFormat", "QET wire format"), + ("diameter_mm", "QetWireDiameterMm", "QET wire diameter in millimeters"), + ("line_width", "QetWireLineWidth", "QET wire view line width"), + ): + value = cleaned_wire_style.get(key) + if value is not None and str(value).strip(): + _set_string(wire, prop_name, str(value).strip(), description) + _set_string( + wire, + "QetRouteDiagnosticsJson", + json.dumps( + _route_payload( + route_data, + collisions, + wire_style_id=wire_style_id, + endpoint_metadata=cleaned_endpoint_metadata, + wire_style=cleaned_wire_style, + ), + ensure_ascii=False, + ), + "Routing connection diagnostics", + ) + if route_data.get("network"): + _set_string( + wire, + "QetRouteNetworkJson", + json.dumps(route_data.get("network", {}), ensure_ascii=False), + "Route network metadata used by this wire", + ) + if route_data.get("route_track"): + _set_string( + wire, + "QetRouteTrackJson", + json.dumps(route_data.get("route_track", {}), ensure_ascii=False), + "Routing carriers passed through by this wire", + ) + + +def build_network_route(start_terminal, end_terminal, route_index=0, options=None, doc=None): + opts = _merged_options(options) + if not opts.get("use_routing_network", True): + return None if doc is None: doc = getattr(start_terminal, "Document", None) or getattr(App, "ActiveDocument", None) if doc is None: return None + constraint_options = _merge_route_constraint_options( + opts, + _document_route_constraint_options(doc), + ) 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) @@ -893,6 +1880,16 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non segment_usage_costs=opts.get("segment_usage_costs", {}), segment_reuse_penalty=float(opts.get("segment_reuse_penalty", 0.0) or 0.0), excluded_transit_carrier_kinds={"TerminalAccess"}, + forbidden_carrier_names=constraint_options.get("forbidden_route_carrier_names", []), + forbidden_carrier_labels=constraint_options.get("forbidden_route_carrier_labels", []), + forbidden_carrier_source_names=constraint_options.get("forbidden_route_carrier_source_names", []), + forbidden_carrier_source_labels=constraint_options.get("forbidden_route_carrier_source_labels", []), + forbidden_carrier_kinds=constraint_options.get("forbidden_route_carrier_kinds", []), + required_carrier_names=constraint_options.get("required_route_carrier_names", []), + required_carrier_labels=constraint_options.get("required_route_carrier_labels", []), + required_carrier_source_names=constraint_options.get("required_route_carrier_source_names", []), + required_carrier_source_labels=constraint_options.get("required_route_carrier_source_labels", []), + required_carrier_kinds=constraint_options.get("required_route_carrier_kinds", []), ) path_keys = path_result.get("path", []) if isinstance(path_result, dict) else [] if not path_keys: @@ -907,25 +1904,39 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non points = [] for point in start_access_points or [start_origin, start_exit]: _append_unique(points, point) - _append_orthogonal(points, carrier_points[0], obstacle_bboxes=candidate_blocked_bboxes) + _append_orthogonal( + points, + carrier_points[0], + obstacle_bboxes=local_access_obstacle_bboxes(points[-1], carrier_points[0]), + ) for point in carrier_points[1:]: _append_unique(points, point) - _append_orthogonal(points, end_exit, obstacle_bboxes=candidate_blocked_bboxes) - for point in reversed(end_access_points or [end_origin, end_exit]): - _append_unique(points, point) - points = _simplify_collinear_points( + _append_orthogonal( points, - preserved_point_keys=_important_route_node_keys(network, path_keys, path_result), + end_exit, + obstacle_bboxes=local_access_obstacle_bboxes(points[-1], end_exit), ) + for point in reversed(end_access_points or [end_origin, end_exit]): + _append_unique(points, point) + preserved_point_keys = _important_route_node_keys(network, path_keys, path_result) + for access_point in list(start_access_points or []) + list(end_access_points or []): + preserved_point_keys.add(_route_point_key(access_point)) + points = _simplify_collinear_points(points, preserved_point_keys=preserved_point_keys) return { "algorithm": "network-dijkstra-v1", "points": points, + "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 []], + }, "network": { "carriers": int(network.get("carrier_count", 0)), "segments": int(network.get("segment_count", 0)), "bridged_segments": int(network.get("bridged_segment_count", 0)), "blocked_segments": int(network.get("blocked_segment_count", 0)), + "boundary_filtered": bool(network.get("boundary_filtered", False)), + "boundary_filtered_segments": int(network.get("boundary_filtered_segment_count", 0) or 0), "nodes": len(network.get("nodes", {})), "entry_distance": float(start_distance or 0.0), "exit_distance": float(end_distance or 0.0), @@ -933,7 +1944,12 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "exit_point_mode": end_mode, "entry_candidate_rank": int(start_candidate_rank or 1), "exit_candidate_rank": int(end_candidate_rank or 1), + "terminal_access_warning_distance": float( + opts.get("terminal_access_warning_distance", 0.0) or 0.0 + ), "obstacle_aware": bool(obstacle_aware), + "boundary_aware": bool(candidate_boundaries), + "route_constraints": _route_constraint_payload(constraint_options), }, "route_track": path_result, "lane": lane, @@ -941,17 +1957,105 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non def route_obstacle_hit_count(points): hits = 0 - if not candidate_blocked_bboxes: + if not route_candidate_blocked_bboxes: return hits for index in range(max(len(points or []) - 1, 0)): start = points[index] end = points[index + 1] - for bbox in candidate_blocked_bboxes: + for bbox in _filter_obstacle_bboxes_near_polyline( + [start, end], + route_candidate_blocked_bboxes, + margin=local_access_obstacle_scan_margin, + ): if _segment_intersects_bbox(start, end, bbox): hits += 1 break return hits + def route_boundary_violation_count(points): + if not candidate_boundaries: + return 0 + violations = 0 + for point in points or []: + if not _point_inside_any_boundary(point, candidate_boundaries): + violations += 1 + return violations + + def route_candidate_identity(candidate): + return ( + candidate.get("projected_key"), + id(candidate.get("carrier")), + ) + + def route_candidate_access_hit_count(access_point, candidate): + if access_point is None or not local_access_blocked_bboxes: + return 0 + point = candidate.get("point") + if point is None: + return 0 + nearby_bboxes = local_access_obstacle_bboxes(access_point, point) + if not nearby_bboxes: + return 0 + access_points = _orthogonal_points_avoiding_obstacles( + access_point, + point, + nearby_bboxes, + ) + return _orthogonal_hit_count(access_points, nearby_bboxes) + + def select_ranked_entry_candidates(route_network, candidates, limit, access_point=None): + ranked = [] + seen_ranked = set() + for candidate in RoutingNetwork.rank_connection_point_candidates(route_network, candidates): + identity = route_candidate_identity(candidate) + if identity in seen_ranked: + continue + seen_ranked.add(identity) + ranked.append(candidate) + selected = list(ranked[:limit]) + if not candidate_boundaries and not route_candidate_blocked_bboxes: + return selected + seen = {route_candidate_identity(candidate) for candidate in selected} + total_limit = int(opts.get("network_entry_candidate_total_limit", 0) or 0) + if total_limit > 0: + total_limit = max(total_limit, limit) + + def can_add_extra_candidate(): + return total_limit <= 0 or len(selected) < total_limit + + added_inside = 0 + for candidate in ranked[limit:]: + if not can_add_extra_candidate(): + break + point = candidate.get("point") + if point is None or not _point_inside_any_boundary(point, candidate_boundaries): + continue + identity = route_candidate_identity(candidate) + if identity in seen: + continue + # 柜内边界存在时,柜外近路径可能挤掉稍远的柜内路径;额外保留柜内候选再交给评分。 + selected.append(candidate) + seen.add(identity) + added_inside += 1 + if added_inside >= limit: + break + added_clear = 0 + for candidate in ranked[limit:]: + if not can_add_extra_candidate(): + break + if route_candidate_access_hit_count(access_point, candidate) > 0: + continue + identity = route_candidate_identity(candidate) + if identity in seen: + continue + # 同理,近入口若都会穿过设备,稍远的干净入口也要进入评分阶段。 + selected.append(candidate) + seen.add(identity) + added_clear += 1 + if added_clear >= limit: + break + return selected + def route_on_network(network, obstacle_aware=False): if network.get("segment_count", 0) <= 0: return None @@ -962,7 +2066,7 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non if terminal_access_limit > 0.0: max_distance = min(max_distance, terminal_access_limit) if max_distance > 0.0 else terminal_access_limit candidate_limit = max(int(opts.get("network_entry_candidate_limit", 8) or 0), 1) - start_candidates = RoutingNetwork.rank_connection_point_candidates( + start_candidates = select_ranked_entry_candidates( network, RoutingNetwork.connection_point_candidates( network, @@ -970,8 +2074,9 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non limit=0, max_distance=max_distance, ), + candidate_limit, + access_point=start_exit, ) - start_candidates = start_candidates[:candidate_limit] if not start_candidates: return None @@ -988,7 +2093,7 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non ) if start_key is None: continue - end_candidates = RoutingNetwork.rank_connection_point_candidates( + end_candidates = select_ranked_entry_candidates( start_network, RoutingNetwork.connection_point_candidates( start_network, @@ -996,8 +2101,9 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non limit=0, max_distance=max_distance, ), + candidate_limit, + access_point=end_exit, ) - end_candidates = end_candidates[:candidate_limit] for end_rank, end_candidate in enumerate(end_candidates, start=1): working_network = clone_route_network(start_network) end_key, end_distance, end_mode = RoutingNetwork.connect_point_candidate_to_network( @@ -1030,7 +2136,14 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non route_score += obstacle_hits * float( opts.get("route_candidate_collision_penalty", 10000.0) or 0.0 ) + boundary_violations = route_boundary_violation_count(route_data.get("points", [])) + route_score += boundary_violations * float( + opts.get("route_candidate_boundary_penalty", 100000.0) or 0.0 + ) route_data["network"]["route_candidate_obstacle_hits"] = int(obstacle_hits) + route_data["network"]["route_candidate_boundary_violations"] = int( + boundary_violations + ) route_data["network"]["entry_candidate_score"] = float(route_score) if best_score is None or route_score < best_score: best_score = route_score @@ -1038,10 +2151,18 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non return best_route use_obstacle_avoidance = bool(opts.get("avoid_obstacles", True)) + use_local_access_obstacle_avoidance = bool(opts.get("avoid_local_access_obstacles", True)) obstacles = [] candidate_obstacles = [] + candidate_boundaries = collect_routing_boundaries(doc, options=opts) + boundary_bboxes = [ + boundary.get("bbox") + for boundary in candidate_boundaries + if isinstance(boundary, dict) and boundary.get("bbox") + ] if use_obstacle_avoidance: obstacles = collect_obstacles(doc, exclude=[start_terminal, end_terminal], options=opts) + if use_obstacle_avoidance or use_local_access_obstacle_avoidance: candidate_options = dict(opts) candidate_options["ignore_endpoint_near_obstacles"] = False candidate_obstacles = collect_obstacles( @@ -1053,6 +2174,48 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non candidate_blocked_bboxes = [ obstacle["bbox"] for obstacle in candidate_obstacles if obstacle.get("bbox") ] + local_access_blocked_bboxes = [ + obstacle["bbox"] + for obstacle in candidate_obstacles + if obstacle.get("bbox") and _is_local_access_obstacle(obstacle) + ] + route_candidate_blocked_bboxes = ( + candidate_blocked_bboxes if use_obstacle_avoidance else local_access_blocked_bboxes + ) + local_access_obstacle_scan_margin = max( + float(opts.get("local_access_obstacle_scan_margin", 0.0) or 0.0), + LOCAL_ACCESS_DETOUR_CLEARANCE, + float(opts.get("obstacle_clearance", 0.0) or 0.0), + ) + + def local_access_obstacle_bboxes(start_point, end_point, preferred_axis=None): + return _local_access_obstacle_bboxes( + start_point, + end_point, + local_access_blocked_bboxes, + preferred_axis=preferred_axis, + margin=local_access_obstacle_scan_margin, + ) + + if boundary_bboxes: + if blocked_bboxes: + boundary_obstacle_network = RoutingNetwork.build_route_graph( + doc, + blocked_bboxes=blocked_bboxes, + allowed_bboxes=boundary_bboxes, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + route_data = route_on_network(boundary_obstacle_network, obstacle_aware=True) + if route_data is not None: + return route_data + boundary_network = RoutingNetwork.build_route_graph( + doc, + allowed_bboxes=boundary_bboxes, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + route_data = route_on_network(boundary_network, obstacle_aware=False) + if route_data is not None: + return route_data if blocked_bboxes: obstacle_aware_network = RoutingNetwork.build_route_graph( @@ -1064,10 +2227,12 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non if route_data is not None: return route_data - network = RoutingNetwork.build_route_graph( - doc, - adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), - ) + network = opts.get("__base_route_network") + if not isinstance(network, dict) or network.get("segment_count", 0) <= 0: + network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) return route_on_network(network, obstacle_aware=False) @@ -1113,6 +2278,118 @@ def _bbox_payload(obj, clearance=0.0): } +def _is_routing_boundary(obj): + try: + return RoutingNetwork.is_routing_boundary(obj) + except Exception: + return False + + +PASS_THROUGH_OBSTACLE_MODES = {"PassThrough", "WireDuctPassThrough", "SupportSurface"} + + +def _routing_obstacle_mode(obj): + return str(getattr(obj, "QetRoutingObstacleMode", "") or "").strip() + + +def _is_auto_detected_support_surface(obj): + try: + return bool(RoutingNetwork._is_support_surface_candidate(obj)) + except Exception: + return False + + +def _object_parent_chain(obj, limit=16): + chain = [] + seen = set() + stack = list(getattr(obj, "InList", []) or []) + while stack and len(chain) < limit: + parent = stack.pop(0) + if parent is None or id(parent) in seen: + continue + seen.add(id(parent)) + chain.append(parent) + stack.extend(list(getattr(parent, "InList", []) or [])) + return chain + + +def _has_pass_through_obstacle_semantics(obj): + if obj is None: + return False + if _routing_obstacle_mode(obj) in PASS_THROUGH_OBSTACLE_MODES: + return True + if _is_auto_detected_support_surface(obj): + return True + for parent in _object_parent_chain(obj): + if _routing_obstacle_mode(parent) in PASS_THROUGH_OBSTACLE_MODES: + return True + if _is_auto_detected_support_surface(parent): + return True + seen = set() + stack = list(getattr(obj, "Group", []) or []) + list(getattr(obj, "OutList", []) or []) + while stack: + child = stack.pop() + if child is None or id(child) in seen: + continue + seen.add(id(child)) + if _routing_obstacle_mode(child) in PASS_THROUGH_OBSTACLE_MODES: + # 真实装配里门板/安装板可能是 LinkGroup/Compound 的子对象; + # 子对象已作为布线面时,父装配体不能再反过来报导线碰撞。 + return True + if _is_auto_detected_support_surface(child): + # 有些旧 FCStd 只生成了路径 carrier,没有给源对象写入 SupportSurface; + # 仍按可识别的支撑面处理,避免柜门/侧盖父装配体成为碰撞误报。 + return True + stack.extend(list(getattr(child, "Group", []) or [])) + stack.extend(list(getattr(child, "OutList", []) or [])) + return False + + +def collect_routing_boundaries(doc, options=None): + # 柜内边界是 FreeCAD 装配态语义,只用于限制 3D 选路,不写入 2D/3D 数据库。 + boundaries = [] + for obj in list(getattr(doc, "Objects", []) or []): + if _is_group(obj) or _is_origin_helper(obj): + continue + if not _is_routing_boundary(obj): + continue + bbox = _bbox_payload(obj, clearance=0.0) + if bbox is None: + continue + boundaries.append( + { + "name": getattr(obj, "Name", ""), + "label": getattr(obj, "Label", ""), + "type_id": getattr(obj, "TypeId", ""), + "bbox": bbox, + } + ) + return boundaries + + +def routing_obstacle_mode_summary(doc): + summary = {} + if doc is None: + return summary + for obj in list(getattr(doc, "Objects", []) or []): + if _is_group(obj) or _is_origin_helper(obj): + continue + mode = _routing_obstacle_mode(obj) + if not mode: + continue + entry = summary.setdefault(mode, {"count": 0, "samples": []}) + entry["count"] += 1 + if len(entry["samples"]) < 8: + entry["samples"].append( + { + "name": getattr(obj, "Name", ""), + "label": getattr(obj, "Label", ""), + "type_id": getattr(obj, "TypeId", ""), + } + ) + return summary + + def _collect_group_tree_ids(root): excluded = set() stack = [root] @@ -1148,6 +2425,31 @@ def _expanded_obstacle_exclusion_ids(doc, exclude): return excluded +def _obstacle_instance_refs(obj): + refs = set() + own = str(getattr(obj, "QetInstanceId", "") or "").strip() + if own: + refs.add(own) + for parent in list(getattr(obj, "InList", []) or []): + parent_instance = str(getattr(parent, "QetInstanceId", "") or "").strip() + if parent_instance: + refs.add(parent_instance) + return refs + + +def _obstacle_parent_refs(obj): + names = [] + labels = [] + for parent in _object_parent_chain(obj, limit=8): + name = str(getattr(parent, "Name", "") or "").strip() + label = str(getattr(parent, "Label", "") or "").strip() + if name and name not in names: + names.append(name) + if label and label not in labels: + labels.append(label) + return {"names": names[:8], "labels": labels[:8]} + + def _distance_point_to_bbox(point, bbox): squared = 0.0 for axis, min_key, max_key in ( @@ -1165,21 +2467,136 @@ def _distance_point_to_bbox(point, bbox): return math.sqrt(squared) -def collect_obstacles(doc, exclude=None, options=None): +def _point_inside_bbox(point, bbox, tolerance=0.000001): + for axis, min_key, max_key in ( + ("x", "xmin", "xmax"), + ("y", "ymin", "ymax"), + ("z", "zmin", "zmax"), + ): + value = _axis_value(point, axis) + if value < float(bbox[min_key]) - tolerance: + return False + if value > float(bbox[max_key]) + tolerance: + return False + return True + + +def _point_inside_any_boundary(point, boundaries): + if not boundaries: + return True + return any( + _point_inside_bbox(point, boundary.get("bbox", {}) or {}) + for boundary in boundaries + if boundary.get("bbox") + ) + + +def _obstacle_candidate_cache(doc, options=None): opts = _merged_options(options) - excluded = _expanded_obstacle_exclusion_ids(doc, exclude) clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0) - endpoint_clearance = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) + clearance - endpoint_points = [] - for obj in exclude or []: - if obj is not None and TerminalObjects.is_terminal_object(obj): - endpoint_points.append(_terminal_origin(obj)) - obstacles = [] + candidates = [] + if doc is None: + return {"clearance": clearance, "candidates": candidates} for obj in list(getattr(doc, "Objects", []) or []): - if id(obj) in excluded: + if _has_pass_through_obstacle_semantics(obj): continue - obstacle_mode = (getattr(obj, "QetRoutingObstacleMode", "") or "").strip() - if obstacle_mode in {"PassThrough", "WireDuctPassThrough", "SupportSurface"}: + if _is_routing_boundary(obj): + continue + if _is_group(obj) or _is_origin_helper(obj): + continue + if TerminalObjects.is_lcs_like(obj) or TerminalObjects.is_terminal_object(obj): + continue + if RoutingNetwork.is_route_carrier(obj) or WiringObjects.is_routed_wire_object(obj): + continue + raw_bbox = _bbox_payload(obj, clearance=0.0) + bbox = _bbox_payload(obj, clearance=clearance) + if bbox is None: + continue + candidates.append( + { + "object_id": id(obj), + "instance_refs": sorted(_obstacle_instance_refs(obj)), + "element_uuid": str(getattr(obj, "QetElementUuid", "") or "").strip(), + "instance_id": str(getattr(obj, "QetInstanceId", "") or "").strip(), + "parent_refs": _obstacle_parent_refs(obj), + "name": getattr(obj, "Name", ""), + "label": getattr(obj, "Label", ""), + "type_id": getattr(obj, "TypeId", ""), + "bbox": bbox, + "raw_bbox": raw_bbox or bbox, + } + ) + return {"clearance": clearance, "candidates": candidates} + + +def _obstacles_from_candidate_cache(cache, exclude=None, options=None): + opts = _merged_options(options) + candidates = [] + if isinstance(cache, dict): + candidates = list(cache.get("candidates", []) or []) + excluded_ids = set(id(obj) for obj in (exclude or []) if obj is not None) + endpoint_instance_ids = { + str(getattr(obj, "QetInstanceId", "") or "").strip() + for obj in (exclude or []) + if obj is not None and str(getattr(obj, "QetInstanceId", "") or "").strip() + } + endpoint_points = [] + for obj in exclude or []: + if obj is not None and TerminalObjects.is_terminal_object(obj): + endpoint_points.append(_terminal_origin(obj)) + clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0) + endpoint_clearance = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) + clearance + obstacles = [] + for candidate in candidates: + if int(candidate.get("object_id", 0) or 0) in excluded_ids: + continue + instance_refs = set(str(item or "").strip() for item in candidate.get("instance_refs", []) or [] if str(item or "").strip()) + if endpoint_instance_ids and instance_refs.intersection(endpoint_instance_ids): + continue + bbox = candidate.get("bbox") + if bbox is None: + continue + if bool(opts.get("ignore_endpoint_near_obstacles", True)) and endpoint_points and any( + _distance_point_to_bbox(point, bbox) <= endpoint_clearance + for point in endpoint_points + ): + continue + obstacles.append( + { + "name": candidate.get("name", ""), + "label": candidate.get("label", ""), + "type_id": candidate.get("type_id", ""), + "element_uuid": str(candidate.get("element_uuid", "") or "").strip(), + "instance_id": str(candidate.get("instance_id", "") or "").strip(), + "parent_refs": candidate.get("parent_refs", {}) + if isinstance(candidate.get("parent_refs", {}), dict) + else {}, + "bbox": bbox, + "raw_bbox": candidate.get("raw_bbox") or bbox, + } + ) + return obstacles + + +def collect_obstacles(doc, exclude=None, options=None): + opts = _merged_options(options) + cache = opts.get("__obstacle_candidate_cache") + if isinstance(cache, dict): + return _obstacles_from_candidate_cache(cache, exclude=exclude, options=opts) + excluded = _expanded_obstacle_exclusion_ids(doc, exclude) + clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0) + endpoint_clearance = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) + clearance + endpoint_points = [] + for obj in exclude or []: + if obj is not None and TerminalObjects.is_terminal_object(obj): + endpoint_points.append(_terminal_origin(obj)) + obstacles = [] + for obj in list(getattr(doc, "Objects", []) or []): + if id(obj) in excluded: + continue + if _has_pass_through_obstacle_semantics(obj): + continue + if _is_routing_boundary(obj): continue if _is_group(obj) or _is_origin_helper(obj): continue @@ -1201,6 +2618,10 @@ def collect_obstacles(doc, exclude=None, options=None): "name": getattr(obj, "Name", ""), "label": getattr(obj, "Label", ""), "type_id": getattr(obj, "TypeId", ""), + # 中文说明:碰撞诊断要能回到 QET 设备和 FreeCAD 实例,便于现场按 2D/3D 绑定定位。 + "element_uuid": str(getattr(obj, "QetElementUuid", "") or "").strip(), + "instance_id": str(getattr(obj, "QetInstanceId", "") or "").strip(), + "parent_refs": _obstacle_parent_refs(obj), "bbox": bbox, "raw_bbox": raw_bbox or bbox, } @@ -1260,6 +2681,14 @@ def detect_collisions(points, obstacles, ignored_segment_indices=None): "collision_kind": collision_kind, "obstacle_name": obstacle.get("name", ""), "obstacle_label": obstacle.get("label", ""), + "obstacle_element_uuid": str(obstacle.get("element_uuid", "") or "").strip(), + "obstacle_instance_id": str(obstacle.get("instance_id", "") or "").strip(), + "obstacle_parent_names": list( + (obstacle.get("parent_refs", {}) or {}).get("names", []) or [] + ), + "obstacle_parent_labels": list( + (obstacle.get("parent_refs", {}) or {}).get("labels", []) or [] + ), "obstacle_bbox": dict(raw_bbox), "collision_bbox": dict(obstacle.get("bbox", {}) or {}), } @@ -1267,13 +2696,86 @@ def detect_collisions(points, obstacles, ignored_segment_indices=None): return collisions -def _endpoint_collision_segment_indices(points): +def _point_close(first, second, tolerance=0.001): + first = _vector(first) + second = _vector(second) + return _distance(first, second) <= float(tolerance or 0.001) + + +def _point_on_segment(point, start, end, tolerance=0.001): + point = _vector(point) + start = _vector(start) + end = _vector(end) + segment_length = _distance(start, end) + if segment_length <= float(tolerance or 0.001): + return _point_close(point, start, tolerance=tolerance) + if _distance(start, point) + _distance(point, end) - segment_length > float(tolerance or 0.001): + return False + return _collinear_points(start, point, end) + + +def _point_on_polyline(point, polyline, tolerance=0.001): + points = [_vector(item) for item in polyline or []] + if not points: + return False + if any(_point_close(point, item, tolerance=tolerance) for item in points): + return True + for index in range(max(len(points) - 1, 0)): + if _point_on_segment(point, points[index], points[index + 1], tolerance=tolerance): + return True + return False + + +def _route_access_points_from_payload(payload): + points = [] + if not isinstance(payload, list): + return points + for item in payload: + if isinstance(item, dict): + try: + points.append(App.Vector(float(item["x"]), float(item["y"]), float(item["z"]))) + except Exception: + continue + return points + + +def _access_collision_segment_indices(points, access_points, from_start=True): + route_points = [_vector(point) for point in points or []] + access_points = [_vector(point) for point in access_points or []] + ignored = set() + if len(route_points) < 2 or len(access_points) < 2: + return ignored + + if from_start: + indices = range(len(route_points) - 1) + else: + indices = range(len(route_points) - 2, -1, -1) + for index in indices: + start = route_points[index] + end = route_points[index + 1] + if not ( + _point_on_polyline(start, access_points) + and _point_on_polyline(end, access_points) + ): + break + ignored.add(index) + return ignored + + +def _endpoint_collision_segment_indices(points, route_data=None): segment_count = max(len(points or []) - 1, 0) if segment_count <= 0: return set() ignored = {0} if segment_count > 1: ignored.add(segment_count - 1) + endpoint_access = route_data.get("endpoint_access", {}) if isinstance(route_data, dict) else {} + if isinstance(endpoint_access, dict): + # 端子局部出线路径允许穿过/贴近设备壳体;碰撞诊断聚焦主路径中段。 + start_access = _route_access_points_from_payload(endpoint_access.get("start_points", [])) + end_access = _route_access_points_from_payload(endpoint_access.get("end_points", [])) + ignored.update(_access_collision_segment_indices(points, start_access, from_start=True)) + ignored.update(_access_collision_segment_indices(points, end_access, from_start=False)) return ignored @@ -1365,22 +2867,255 @@ def _set_task_status(task, status): ) -def _style_wire(wire, collision_count=0): +def _parse_line_color(value): + text = str(value or "").strip() + if not text: + return None + lower = text.lower() + named = { + "black": (0.0, 0.0, 0.0), + "white": (1.0, 1.0, 1.0), + "red": (1.0, 0.0, 0.0), + "green": (0.0, 0.6, 0.0), + "blue": (0.0, 0.35, 1.0), + "yellow": (1.0, 0.85, 0.0), + "orange": (1.0, 0.55, 0.0), + "brown": (0.45, 0.25, 0.1), + "gray": (0.5, 0.5, 0.5), + "grey": (0.5, 0.5, 0.5), + } + if lower in named: + return named[lower] + if lower.isdigit() and len(lower) > 6: + try: + number = int(lower, 10) + except Exception: + number = -1 + if 0 <= number <= 0xFFFFFFFF: + raw = "{0:06x}".format(number & 0xFFFFFF) + return tuple(round(int(raw[index:index + 2], 16) / 255.0, 6) for index in (0, 2, 4)) + # QET 数据库里的颜色可能带 #,也可能直接保存为 3366CC / 0x3366CC。 + raw = lower + if raw.startswith("#"): + raw = raw[1:] + elif raw.startswith("0x"): + raw = raw[2:] + if raw != lower or (len(raw) in (3, 6, 8) and all(ch in "0123456789abcdef" for ch in raw)): + if len(raw) == 8: + raw = raw[2:] + if len(raw) == 3: + raw = "".join(ch * 2 for ch in raw) + if len(raw) == 6: + try: + return tuple(round(int(raw[index:index + 2], 16) / 255.0, 6) for index in (0, 2, 4)) + except Exception: + return None + for prefix, suffix in (("rgb(", ")"), ("rgba(", ")")): + if lower.startswith(prefix) and lower.endswith(suffix): + text = lower[len(prefix):-len(suffix)] + break + values = [part.strip() for part in text.replace(";", ",").split(",")] + if len(values) >= 3: + try: + numbers = [float(values[index]) for index in range(3)] + except Exception: + numbers = [] + if len(numbers) == 3: + if max(numbers) > 1.0: + numbers = [value / 255.0 for value in numbers] + return tuple(max(0.0, min(1.0, round(value, 6))) for value in numbers) + return None + + +def _wire_style_line_width(wire_style): + if not isinstance(wire_style, dict): + return None + for key in ("line_width", "diameter_mm"): + try: + width = float(wire_style.get(key, 0) or 0) + except Exception: + width = 0.0 + if width > 0.0: + return width + area_mm2 = _wire_style_area_mm2(wire_style.get("area_or_spec", "")) + if area_mm2 > 0.0: + return math.sqrt(4.0 * area_mm2 / math.pi) + return None + + +def _wire_style_area_mm2(value): + # QET 有时只有 "2.5mm2" 这类规格文本,第一版用它估算显示线宽。 + text = str(value or "").strip().lower() + if not text: + return 0.0 + text = text.replace(",", ".").replace("\u00b2", "2") + matches = re.findall(r"([0-9]+(?:\.[0-9]+)?)\s*(?:mm\s*(?:2|\^2)|平方)", text) + if not matches and re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", text): + matches = [text] + for match in matches: + try: + area = float(match) + except Exception: + area = 0.0 + if area > 0.0: + return area + return 0.0 + + +def _wire_style_color(wire_style): + if not isinstance(wire_style, dict): + return None + return _parse_line_color(wire_style.get("line_color", "")) + + +def _wire_style_draw_style(wire_style): + if not isinstance(wire_style, dict): + return "Solid" + text = str(wire_style.get("line_type", "") or "").strip().lower() + if not text: + return "Solid" + normalized = text.replace("_", "").replace("-", "").replace(" ", "") + if normalized in {"dashline", "dashed", "dash", "dashes", "虚线"}: + return "Dashed" + if normalized in {"dotline", "dotted", "dot", "dots", "点线"}: + return "Dotted" + if normalized in {"dashdotline", "dashdot", "点划线"}: + return "Dashdot" + return "Solid" + + +def _resolve_wire_style_from_database(wire_style_id, database_path="", project_uuid=""): + style_id = str(wire_style_id or "").strip() + db_path = str(database_path or "").strip() + if not style_id or not db_path: + return {} + try: + connection = sqlite3.connect(db_path) + connection.row_factory = sqlite3.Row + except Exception: + return {} + try: + if str(project_uuid or "").strip(): + row = connection.execute( + """ + SELECT * + FROM wire_properties + WHERE id = ? + AND (project_uuid = ? OR project_uuid = '' OR project_uuid IS NULL) + ORDER BY CASE WHEN project_uuid = ? THEN 0 ELSE 1 END + LIMIT 1 + """, + (style_id, project_uuid, project_uuid), + ).fetchone() + else: + row = connection.execute( + "SELECT * FROM wire_properties WHERE id = ? LIMIT 1", + (style_id,), + ).fetchone() + if row is None: + return {} + payload = {key: row[key] for key in row.keys()} + payload["id"] = str(payload.get("id", style_id)) + return _clean_wire_style_payload(payload) + except Exception: + return {} + finally: + try: + connection.close() + except Exception: + pass + + +def resolve_wire_style(wire_style_id, options=None, project_uuid=""): + style_id = str(wire_style_id or "").strip() + if not style_id: + return {} + opts = options or {} + cache = opts.get("__wire_style_cache") + cache_key = ( + style_id, + str(project_uuid or "").strip(), + str(opts.get("wire_style_database_path", "") or os.environ.get("QET_WIRE_PROPERTIES_DB", "") or "").strip(), + id(opts.get("wire_style_lookup")) if callable(opts.get("wire_style_lookup")) else "", + ) + if isinstance(cache, dict) and cache_key in cache: + return dict(cache.get(cache_key) or {}) + lookup = opts.get("wire_style_lookup") + if callable(lookup): + try: + payload = _clean_wire_style_payload(lookup(style_id, project_uuid)) + except TypeError: + payload = _clean_wire_style_payload(lookup(style_id)) + except Exception: + payload = {} + if isinstance(cache, dict): + cache[cache_key] = dict(payload) + return payload + styles = opts.get("wire_styles") + if isinstance(styles, dict): + style = styles.get(style_id) + if style is None: + try: + style = styles.get(int(style_id)) + except Exception: + style = None + if isinstance(style, dict): + payload = dict(style) + payload.setdefault("id", style_id) + payload = _clean_wire_style_payload(payload) + if isinstance(cache, dict): + cache[cache_key] = dict(payload) + return payload + payload = _resolve_wire_style_from_database( + style_id, + database_path=opts.get("wire_style_database_path", "") + or os.environ.get("QET_WIRE_PROPERTIES_DB", ""), + project_uuid=project_uuid, + ) + if isinstance(cache, dict): + cache[cache_key] = dict(payload) + return payload + + +def _style_wire(wire, collision_count=0, wire_style=None): try: wire.ViewObject.Visibility = True - wire.ViewObject.LineWidth = 5.0 + wire.ViewObject.LineWidth = _wire_style_line_width(wire_style) or 5.0 if hasattr(wire.ViewObject, "DrawStyle"): - wire.ViewObject.DrawStyle = "Solid" + wire.ViewObject.DrawStyle = _wire_style_draw_style(wire_style) if hasattr(wire.ViewObject, "DisplayMode"): wire.ViewObject.DisplayMode = "Wireframe" if collision_count: wire.ViewObject.LineColor = (1.0, 0.1, 0.0) else: - wire.ViewObject.LineColor = (0.0, 0.35, 1.0) + wire.ViewObject.LineColor = _wire_style_color(wire_style) or (0.0, 0.35, 1.0) except Exception: pass +def _wire_display_label(start_terminal, end_terminal, wire_label="", wire_mark="", wire_uuid="", status=""): + base = ( + str(wire_label or "").strip() + or str(wire_mark or "").strip() + or str(wire_uuid or "").strip() + or "QET Routed Connection" + ) + start_label = ( + str(getattr(start_terminal, "Label", "") or "").strip() + or str(getattr(start_terminal, "Name", "") or "").strip() + ) + end_label = ( + str(getattr(end_terminal, "Label", "") or "").strip() + or str(getattr(end_terminal, "Name", "") or "").strip() + ) + if start_label and end_label: + base = "{0}: {1} -> {2}".format(base, start_label, end_label) + status_text = str(status or "").strip() + if status_text: + base = "{0} ({1})".format(base, status_text) + return base + + def route_eplan_connection_between_terminals( doc, start_terminal, @@ -1413,6 +3148,7 @@ def route_eplan_connection_between_terminals( project_uuid = _project_uuid(doc, start_terminal, end_terminal) if not project_uuid: raise AutoRoutingError("Project UUID is required for routing connections.") + wire_style = resolve_wire_style(effective_wire_style_id, options=opts, project_uuid=project_uuid) route_data = build_network_route( start_terminal, @@ -1422,6 +3158,12 @@ def route_eplan_connection_between_terminals( doc=doc, ) if route_data is None: + if _has_route_constraints(opts) or _has_route_constraints( + _document_route_constraint_options(doc) + ): + raise AutoRoutingError( + "没有满足路径约束的布线路径网络;请检查 required/forbidden 路径规则、线槽和 UserPath 是否连通。" + ) raise AutoRoutingError( "没有可用的布线路径网络;请先生成布线布局空间和布线路径网络。" ) @@ -1433,8 +3175,23 @@ def route_eplan_connection_between_terminals( obstacles = collect_obstacles(doc, exclude=[start_terminal, end_terminal], options=opts) ignored_collision_segments = set() if opts.get("ignore_endpoint_collision_segments", True): - ignored_collision_segments = _endpoint_collision_segment_indices(points) + ignored_collision_segments = _endpoint_collision_segment_indices(points, route_data=route_data) collisions = detect_collisions(points, obstacles, ignored_segment_indices=ignored_collision_segments) + route_source_labels = _route_source_labels(route_data.get("route_track", {}), limit=4) + if route_source_labels: + collisions = [ + dict(collision, route_source_labels=route_source_labels) + for collision in collisions + ] + collisions = _collisions_with_endpoint_relations( + collisions, + endpoint_metadata=endpoint_metadata, + ) + collisions, auto_ignored_collisions = _filter_auto_ignored_collisions(collisions, opts) + if auto_ignored_collisions: + network_payload = route_data.setdefault("network", {}) + if isinstance(network_payload, dict): + network_payload["auto_ignored_unbound_structural_obstacle_collisions"] = len(auto_ignored_collisions) status = "CollisionWarning" if collisions else "Routed" existing_replacements = [] @@ -1451,7 +3208,14 @@ def route_eplan_connection_between_terminals( removed_existing = 0 try: wire = _create_wire_geometry(doc, wire_name, points) - wire.Label = wire_label or wire_mark or wire_uuid or "QET Routed Connection" + wire.Label = _wire_display_label( + start_terminal, + end_terminal, + wire_label=wire_label, + wire_mark=wire_mark, + wire_uuid=wire_uuid, + status=status, + ) WiringObjects.set_routed_wire_semantics( wire, project_uuid, @@ -1475,6 +3239,7 @@ def route_eplan_connection_between_terminals( collisions, wire_style_id=effective_wire_style_id, endpoint_metadata=endpoint_metadata, + wire_style=wire_style, ) routed_group = WiringObjects.ensure_routed_group(doc, project_uuid) @@ -1484,7 +3249,7 @@ def route_eplan_connection_between_terminals( routed_group.ViewObject.Visibility = True except Exception: pass - _style_wire(wire, collision_count=len(collisions)) + _style_wire(wire, collision_count=len(collisions), wire_style=wire_style) task = _find_task_by_wire_uuid(doc, wire_uuid) _set_task_status(task, status) @@ -1508,7 +3273,10 @@ def route_eplan_connection_between_terminals( return { "wire": wire, + "wire_object_label": str(getattr(wire, "Label", "") or "").strip(), "route_status": status, + "wire_style_status": _wire_style_status(effective_wire_style_id, wire_style), + "wire_style": _clean_wire_style_payload(wire_style), "algorithm": route_data.get("algorithm", ""), "network": route_data.get("network", {}), "route_track": route_data.get("route_track", {}), @@ -1517,6 +3285,7 @@ def route_eplan_connection_between_terminals( "length_mm": _route_length(points), "collision_count": len(collisions), "collisions": collisions, + "auto_ignored_collisions": auto_ignored_collisions, "replaced_routed_connections": removed_existing, } @@ -1531,6 +3300,167 @@ def _wire_item_value(item, *names): return "" +def _payload_device_index(payload): + index = {"by_element": {}, "by_instance": {}} + if not isinstance(payload, dict): + return index + for device in list(payload.get("devices", []) or []): + if not isinstance(device, dict): + continue + element_uuid = str(device.get("element_uuid", "") or "").strip() + instance_id = str(device.get("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 + return index + + +def _payload_device_for_endpoint(device_index, item, side): + if not isinstance(device_index, dict): + return {} + element_uuid = _wire_item_value(item, "{0}_element_uuid".format(side)) + instance_id = _wire_item_value(item, "{0}_instance_id".format(side)) + if instance_id: + device = (device_index.get("by_instance", {}) or {}).get(instance_id) + if isinstance(device, dict): + return device + if element_uuid: + device = (device_index.get("by_element", {}) or {}).get(element_uuid) + if isinstance(device, dict): + return device + return {} + + +def _payload_device_value(device, *names): + if not isinstance(device, dict): + return "" + for name in names: + value = device.get(name, "") + if value is not None and str(value).strip(): + return str(value).strip() + return "" + + +def _collision_relation(sample): + if not isinstance(sample, dict): + return "unknown_collision_relation" + obstacle_element_uuid = str(sample.get("obstacle_element_uuid", "") or "").strip() + if not obstacle_element_uuid: + return "unbound_obstacle_collision" + endpoint_element_uuids = { + str(sample.get("start_element_uuid", "") or "").strip(), + str(sample.get("end_element_uuid", "") or "").strip(), + } + endpoint_element_uuids.discard("") + if obstacle_element_uuid in endpoint_element_uuids: + return "endpoint_device_collision" + return "third_party_device_collision" + + +def _is_auto_ignorable_unbound_structural_collision(collision): + if not isinstance(collision, dict): + return False + if str(collision.get("obstacle_element_uuid", "") or "").strip(): + return False + relation = str(collision.get("collision_relation", "") or "").strip() + if relation and relation != "unbound_obstacle_collision": + return False + own_text = " ".join( + [ + str(collision.get("obstacle_label", "") or ""), + str(collision.get("obstacle_name", "") or ""), + ] + ).lower() + if any(keyword in own_text for keyword in _DEVICE_COLLISION_KEYWORDS): + return False + text_parts = [ + collision.get("obstacle_label", ""), + collision.get("obstacle_name", ""), + ] + text_parts.extend(list(collision.get("obstacle_parent_labels", []) or [])) + text_parts.extend(list(collision.get("obstacle_parent_names", []) or [])) + text = " ".join(str(part or "").lower() for part in text_parts) + if not any(keyword in text for keyword in _STRUCTURAL_COLLISION_KEYWORDS): + return False + imported_context = " ".join( + str(part or "").lower() + for part in list(collision.get("obstacle_parent_labels", []) or []) + + list(collision.get("obstacle_parent_names", []) or []) + ) + imported_markers = ( + "qet exchange devices", + "qetexchangedevices", + "qetcabinet", + "linkgroup", + "compound", + "nauo", + ) + return any(marker in imported_context for marker in imported_markers) + + +def _filter_auto_ignored_collisions(collisions, options=None): + if not bool((options or {}).get("auto_ignore_unbound_structural_obstacles", True)): + return list(collisions or []), [] + kept = [] + ignored = [] + for collision in list(collisions or []): + if _is_auto_ignorable_unbound_structural_collision(collision): + ignored.append(collision) + else: + kept.append(collision) + return kept, ignored + + +def _result_collision_count(result): + if not isinstance(result, dict): + return 0 + try: + return int(result.get("collision_count", 0) or 0) + except Exception: + return len(list(result.get("collisions", []) or [])) + + +def _result_third_party_collision_count(result, item): + if not isinstance(result, dict): + return 0 + count = 0 + for collision in list(result.get("collisions", []) or []): + if not isinstance(collision, dict): + continue + sample = dict(collision) + sample.update( + { + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + } + ) + if _collision_relation(sample) == "third_party_device_collision": + count += 1 + return count + + +def _collisions_with_endpoint_relations(collisions, endpoint_metadata=None): + metadata = _clean_endpoint_metadata(endpoint_metadata) + if not metadata: + return list(collisions or []) + enriched = [] + for collision in list(collisions or []): + if not isinstance(collision, dict): + enriched.append(collision) + continue + sample = dict(collision) + sample.update( + { + "start_element_uuid": metadata.get("start_element_uuid", ""), + "end_element_uuid": metadata.get("end_element_uuid", ""), + } + ) + sample["collision_relation"] = _collision_relation(sample) + enriched.append(sample) + return enriched + + def _route_lane_key(start_uuid, end_uuid): endpoints = sorted( value @@ -1647,949 +3577,5131 @@ def format_terminal_binding_report(report): return message -def route_eplan_connections_from_payload(doc, payload, options=None, prepared_layout=None): - if doc is None: - raise AutoRoutingError("No FreeCAD document is available.") - if not isinstance(payload, dict): - raise AutoRoutingError("Exchange payload must be an object.") - - opts = _merged_options(options) - terminal_binding_report = bind_wire_task_terminals_from_payload(doc, payload) - terminals = index_terminals(doc) - local_terminal_count = sum( - 1 - for terminal_uuid in terminals - if TerminalObjects.is_local_terminal_uuid(terminal_uuid) - ) - wires = payload.get("wires", []) or [] - report = { - "total_wires": len(wires), - "available_terminals": len(terminals), - "local_terminals": local_terminal_count, - "auto_bound_terminals": terminal_binding_report["bound"], - "auto_created_terminals": terminal_binding_report["created"], - "auto_terminal_binding_warnings": terminal_binding_report["warnings"], - "routed": 0, - "collision_warnings": 0, - "replaced_routed_connections": 0, - "total_length_mm": 0.0, - "skipped_missing_terminal": 0, - "skipped_missing_route_network": 0, - "skipped_invalid": 0, - "terminal_access_warning_distance": float( - opts.get( - "terminal_access_warning_distance", - RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE, - ) - or 0.0 - ), - "missing_endpoint_uuids": [], - "missing_endpoint_samples": [], - "missing_route_network_samples": [], - "collision_samples": [], - "errors": [], - "error_samples": [], - "route_status_counts": {}, - "routes": [], +def _wire_style_database_status(database_path): + path = str(database_path or "").strip() + status = { + "path": path, + "status": "NotConfigured" if not path else "Available", + "has_wire_properties_table": False, + "wire_properties_count": None, } - if isinstance(prepared_layout, dict): - report["prepared_layout"] = prepared_layout + if not path: + return status + if not os.path.exists(path): + status["status"] = "Missing" + return status try: - route_network = RoutingNetwork.build_route_graph( - doc, - adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), - ) + connection = sqlite3.connect(path) except Exception as exc: - route_network = {} - report["route_network_error"] = str(exc) - report["route_network_carriers"] = int(route_network.get("carrier_count", 0) or 0) - report["route_network_segments"] = int(route_network.get("segment_count", 0) or 0) - report["route_network_nodes"] = len(route_network.get("nodes", {}) or {}) - has_route_network = report["route_network_segments"] > 0 - missing_endpoint_uuids = set() - lane_indexes_by_pair = {} - lane_indexes_by_segment = {} - segment_usage_costs = _existing_routed_segment_usage( - doc, - excluded_wire_uuids=_incoming_wire_uuids(wires), - ) + status["status"] = "Unreadable" + status["error"] = str(exc) + return status + try: + row = connection.execute( + """ + SELECT name + FROM sqlite_master + WHERE type = 'table' + AND name = 'wire_properties' + LIMIT 1 + """ + ).fetchone() + if row is None: + status["status"] = "NoWirePropertiesTable" + else: + status["has_wire_properties_table"] = True + # 现场排障需要区分“库存在但没有任何导线样式”和“样式 ID 查不到”。 + count_row = connection.execute("SELECT COUNT(*) FROM wire_properties").fetchone() + status["wire_properties_count"] = int((count_row or [0])[0] or 0) + if status["wire_properties_count"] <= 0: + status["status"] = "EmptyWirePropertiesTable" + except Exception as exc: + status["status"] = "Unreadable" + status["error"] = str(exc) + finally: + try: + connection.close() + except Exception: + pass + return status - def add_status(status): - key = str(status or "").strip() or "Unknown" - report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 - def set_item_task_status(item, status): - wire_uuid = _wire_item_value(item, "wire_id", "wire_uuid", "id") - if not wire_uuid: - return - _set_task_status(_find_task_by_wire_uuid(doc, wire_uuid), status) +def _normalized_filesystem_path(path): + text = str(path or "").strip() + if not text: + return "" + try: + return os.path.normcase(os.path.abspath(text)) + except Exception: + return os.path.normcase(text) - def missing_route_network_sample(item, start_uuid, end_uuid, error_text=""): - sample = { - "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), - "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), - "start_terminal_uuid": start_uuid, - "start_element_uuid": _wire_item_value(item, "start_element_uuid"), - "start_terminal_display": _wire_item_value(item, "start_terminal_display"), - "start_device_label": _wire_item_value(item, "start_device_label"), - "end_terminal_uuid": end_uuid, - "end_element_uuid": _wire_item_value(item, "end_element_uuid"), - "end_terminal_display": _wire_item_value(item, "end_terminal_display"), - "end_device_label": _wire_item_value(item, "end_device_label"), - "endpoint_label": _wire_item_value(item, "endpoint_label"), - } - if error_text: - sample["error"] = error_text - return sample - def add_missing_route_network_sample(item, start_uuid, end_uuid, error_text=""): - if len(report["missing_route_network_samples"]) >= 8: - return - report["missing_route_network_samples"].append( - missing_route_network_sample(item, start_uuid, end_uuid, error_text=error_text) +def _wire_style_ids_from_wires(wires): + style_ids = [] + seen = set() + for item in wires or []: + if not isinstance(item, dict): + continue + style_id = _wire_item_value(item, "wire_style_id") + if not style_id or style_id in seen: + continue + seen.add(style_id) + style_ids.append(style_id) + return style_ids + + +def _wire_style_database_resolves_style_ids(database_path, style_ids, project_uuid=""): + requested_ids = [str(item or "").strip() for item in (style_ids or []) if str(item or "").strip()] + if not requested_ids: + return True + for style_id in requested_ids: + if not _resolve_wire_style_from_database(style_id, database_path=database_path, project_uuid=project_uuid): + return False + return True + + +def _discover_wire_style_database_path_from_json_path( + json_path, + project_uuid="", + style_ids=None, + exclude_paths=None, +): + try: + directory = os.path.dirname(os.path.abspath(str(json_path or ""))) + except Exception: + return "" + if not directory: + return "" + search_dirs = [] + for base in ( + directory, + os.path.dirname(directory), + os.path.dirname(os.path.dirname(directory)), + ): + if not base or base in search_dirs: + continue + search_dirs.append(base) + data_dir = os.path.join(base, "datafiles") + if data_dir not in search_dirs: + search_dirs.append(data_dir) + parent = os.path.dirname(directory) + for base in (parent, os.path.dirname(parent)): + if not base: + continue + try: + for name in sorted(os.listdir(base)): + data_dir = os.path.join(base, name, "datafiles") + if os.path.isdir(data_dir) and data_dir not in search_dirs: + search_dirs.append(data_dir) + except Exception: + pass + candidates = [] + for search_dir in search_dirs: + try: + names = os.listdir(search_dir) + except Exception: + continue + priority = {"project-local.db": 0, "project-local.sqlite": 1} + for name in names: + lower = name.lower() + if lower in priority or lower.endswith((".sqlite", ".sqlite3", ".db")): + candidates.append((priority.get(lower, 10), lower, os.path.join(search_dir, name))) + excluded = { + _normalized_filesystem_path(path) + for path in (exclude_paths or []) + if str(path or "").strip() + } + for _priority, _name, candidate in sorted(candidates): + if _normalized_filesystem_path(candidate) in excluded: + continue + # 只自动采用确实含 wire_properties 的库,避免把其它业务库误当成导线样式库。 + candidate_status = _wire_style_database_status(candidate) + if candidate_status.get("status", "") != "Available": + continue + if not _wire_style_database_resolves_style_ids( + candidate, + style_ids=style_ids, + project_uuid=project_uuid, + ): + continue + return candidate + return "" + + +def _context_wire_style_database_path(project_uuid="", style_ids=None, exclude_paths=None): + summary = getattr(App, "_qet_exchange_summary", None) + if not isinstance(summary, dict): + return "" + style_path = str(summary.get("wire_style_database_path", "") or "").strip() + excluded = { + _normalized_filesystem_path(path) + for path in (exclude_paths or []) + if str(path or "").strip() + } + if ( + style_path + and _normalized_filesystem_path(style_path) not in excluded + and _wire_style_database_status(style_path).get("status", "") == "Available" + and _wire_style_database_resolves_style_ids( + style_path, + style_ids=style_ids, + project_uuid=project_uuid, ) + ): + return style_path + return _discover_wire_style_database_path_from_json_path( + summary.get("json_path", ""), + project_uuid=project_uuid, + style_ids=style_ids, + exclude_paths=exclude_paths, + ) - def is_missing_route_network_error(error_text): - text = str(error_text or "") - return "没有可用的布线路径网络" in text or "No route path" in text - def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): - route_options = dict(options or {}) - if isinstance(item, dict) and "__segment_usage_costs" in item: - route_options["segment_usage_costs"] = item.get("__segment_usage_costs", {}) - return route_eplan_connection_between_terminals( - doc, - start_terminal, - end_terminal, - route_index=route_lane_index, - options=route_options, - wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), - wire_label=_wire_item_value(item, "wire_label", "wire_mark"), - net_uuid=_wire_item_value(item, "net_uuid"), - group_uuid=_wire_item_value(item, "group_uuid"), - wire_mark=_wire_item_value(item, "wire_mark"), - wire_mark_is_manual=bool(item.get("wire_mark_is_manual", False)), - wire_style_id=_wire_item_value(item, "wire_style_id"), - endpoint_metadata=endpoint_metadata, - defer_recompute=True, +def _apply_wire_style_database_option(opts, payload): + if not isinstance(opts, dict): + return opts + project_uuid = "" + style_ids = [] + if isinstance(payload, dict): + project_uuid = str(payload.get("project_uuid", "") or "").strip() + style_ids = _wire_style_ids_from_wires(payload.get("wires", []) or []) + if isinstance(payload, dict): + for payload_key in ("wire_style_database_path", "project_database_path", "database_path"): + payload_db_path = str(payload.get(payload_key, "") or "").strip() + if payload_db_path and not str(opts.get("wire_style_database_path", "") or "").strip(): + opts["wire_style_database_path"] = payload_db_path + break + configured_path = str(opts.get("wire_style_database_path", "") or "").strip() + if configured_path: + configured_status = _wire_style_database_status(configured_path) + if configured_status.get("status", "") != "Available": + fallback_path = _context_wire_style_database_path( + project_uuid=project_uuid, + style_ids=style_ids, + exclude_paths=[configured_path], + ) + if fallback_path: + opts["wire_style_database_fallback_from"] = configured_path + opts["wire_style_database_path"] = fallback_path + return opts + context_db_path = _context_wire_style_database_path(project_uuid=project_uuid, style_ids=style_ids) + if context_db_path and not str(opts.get("wire_style_database_path", "") or "").strip(): + opts["wire_style_database_path"] = context_db_path + return opts + + +def _payload_wires_have_style_ids(payload): + if not isinstance(payload, dict) or not isinstance(payload.get("wires"), list): + return False + for item in payload.get("wires") or []: + if isinstance(item, dict) and _wire_item_value(item, "wire_style_id"): + return True + return False + + +def _context_exchange_json_path(): + summary = getattr(App, "_qet_exchange_summary", None) + if isinstance(summary, dict): + json_path = str(summary.get("json_path", "") or "").strip() + if json_path: + return json_path + return os.environ.get("QET_2D_TO_3D_JSON", "").strip() + + +def _load_context_exchange_payload(): + json_path = _context_exchange_json_path() + if not json_path: + return {} + try: + with open(json_path, "r", encoding="utf-8") as handle: + payload = json.load(handle) + except Exception: + return {} + return payload if isinstance(payload, dict) else {} + + +def _context_payload_matches_project(payload, context_payload): + payload_project_uuid = "" + if isinstance(payload, dict): + payload_project_uuid = str(payload.get("project_uuid", "") or "").strip() + context_project_uuid = "" + if isinstance(context_payload, dict): + context_project_uuid = str(context_payload.get("project_uuid", "") or "").strip() + if payload_project_uuid and context_project_uuid and payload_project_uuid != context_project_uuid: + # 手动打开其它 FCStd 后,全局 QET 会话可能仍指向旧 JSON;项目不一致时禁止回补。 + return False + return True + + +def _load_context_payload_with_wire_styles(payload): + if _payload_wires_have_style_ids(payload): + return payload + context_payload = _load_context_exchange_payload() + if not _payload_wires_have_style_ids(context_payload): + return payload + if not _context_payload_matches_project(payload, context_payload): + return payload + # 当前 FreeCAD 会话可能早于样式字段加载;只在磁盘 JSON 确实有样式时回补。 + result = dict(context_payload) + devices = list(result.get("devices", []) or []) + if devices: + result["__context_devices_json_path"] = _context_exchange_json_path() + result["__context_device_count"] = len(devices) + return result + + +def _load_context_payload_with_devices(payload): + if not isinstance(payload, dict): + return payload + if isinstance(payload.get("devices"), list) and payload.get("devices"): + return payload + json_path = _context_exchange_json_path() + context_payload = _load_context_exchange_payload() + devices = list(context_payload.get("devices", []) or []) if isinstance(context_payload, dict) else [] + if not devices or not _context_payload_matches_project(payload, context_payload): + return payload + # 只补设备列表,保留当前 FCStd 任务导线,避免用磁盘 JSON 覆盖用户正在测试的任务对象。 + merged = dict(payload) + merged["devices"] = devices + merged["__context_devices_json_path"] = json_path + merged["__context_device_count"] = len(devices) + return merged + + +def _preflight_wire_payload(doc, payload): + doc_project_uuid = _project_uuid(doc) + payload_project_uuid = "" + if isinstance(payload, dict): + payload_project_uuid = str(payload.get("project_uuid", "") or "").strip() + if doc_project_uuid and payload_project_uuid and doc_project_uuid != payload_project_uuid: + task_payload = _wire_tasks_payload(doc) + return task_payload, list(task_payload.get("wires") or []), "tasks" + payload = _load_context_payload_with_wire_styles(payload) + payload = _load_context_payload_with_devices(payload) + if isinstance(payload, dict) and isinstance(payload.get("wires"), list): + return payload, list(payload.get("wires") or []), "payload" + task_payload = _wire_tasks_payload(doc) + return task_payload, list(task_payload.get("wires") or []), "tasks" + + +def _payload_matches_document_project(doc, payload): + if not isinstance(payload, dict): + return False + doc_project_uuid = _project_uuid(doc) + payload_project_uuid = str(payload.get("project_uuid", "") or "").strip() + if doc_project_uuid and payload_project_uuid and doc_project_uuid != payload_project_uuid: + return False + return True + + +def _append_preflight_issue(report, code, message, severity="warning", count=1, samples=None): + if code in report.get("issue_codes", []): + return + issue = { + "severity": severity, + "code": code, + "message": message, + "count": int(count or 0), + } + if samples: + issue["samples"] = list(samples) + report["issues"].append(issue) + report["issue_codes"].append(code) + + +def _append_preflight_path_network_issues(report, diagnostic): + if not isinstance(diagnostic, dict): + return + preflight_blocking_codes = { + "route_carriers_outside_boundary", + "terminals_outside_boundary", + } + for issue in _dict_items(diagnostic.get("issues", []) or []): + code = str(issue.get("code", "") or "").strip() + if not code or code not in preflight_blocking_codes: + continue + samples = [] + if code == "route_carriers_outside_boundary": + samples = diagnostic.get("route_carriers_outside_boundary", []) or [] + elif code == "terminals_outside_boundary": + samples = diagnostic.get("terminals_outside_boundary", []) or [] + _append_preflight_issue( + report, + code, + _routing_path_network_issue_label(code), + severity=issue.get("severity", "warning"), + count=issue.get("count", 1), + samples=samples, ) + +def _wire_style_preflight_summary(wires, options, project_uuid, database_status): + summary = { + "with_style_id": 0, + "without_style_id": 0, + "resolved": 0, + "missing": 0, + "unique_style_ids": [], + "missing_style_ids": [], + "missing_samples": [], + } + style_ids = [] + seen_ids = set() for item in wires: if not isinstance(item, dict): - report["skipped_invalid"] += 1 - add_status("Invalid") continue - start_uuid = _wire_item_value(item, "start_terminal_uuid") - end_uuid = _wire_item_value(item, "end_terminal_uuid") - start_terminal = terminals.get(start_uuid) - end_terminal = terminals.get(end_uuid) - if 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: - missing_endpoint_uuids.add(terminal_uuid) - # 这里只保留少量样例,避免面板状态被大量导线任务刷屏。 - if len(report["missing_endpoint_samples"]) < 8: - report["missing_endpoint_samples"].append( + style_id = _wire_item_value(item, "wire_style_id") + if not style_id: + summary["without_style_id"] += 1 + continue + summary["with_style_id"] += 1 + if style_id not in seen_ids: + seen_ids.add(style_id) + style_ids.append(style_id) + summary["unique_style_ids"] = list(style_ids) + if not style_ids: + return summary + + can_lookup = ( + callable((options or {}).get("wire_style_lookup")) + or isinstance((options or {}).get("wire_styles"), dict) + or bool(database_status.get("has_wire_properties_table", False)) + ) + if not can_lookup: + summary["missing"] = summary["with_style_id"] + summary["missing_style_ids"] = list(style_ids) + for item in wires: + if not isinstance(item, dict): + continue + style_id = _wire_item_value(item, "wire_style_id") + if style_id and len(summary["missing_samples"]) < 8: + summary["missing_samples"].append( { "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), - "start_terminal_uuid": start_uuid, - "start_found": start_terminal is not None, - "start_element_uuid": _wire_item_value(item, "start_element_uuid"), - "start_terminal_display": _wire_item_value(item, "start_terminal_display"), - "end_terminal_uuid": end_uuid, - "end_found": end_terminal is not None, - "end_element_uuid": _wire_item_value(item, "end_element_uuid"), - "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "wire_style_id": style_id, } ) + return summary + + resolved_ids = set() + missing_ids = set() + for style_id in style_ids: + style = resolve_wire_style(style_id, options=options, project_uuid=project_uuid) + if isinstance(style, dict) and style: + resolved_ids.add(style_id) + else: + missing_ids.add(style_id) + for item in wires: + if not isinstance(item, dict): continue - if not has_route_network: - report["skipped_missing_route_network"] += 1 - add_status("MissingRouteNetwork") - set_item_task_status(item, "MissingRouteNetwork") - add_missing_route_network_sample(item, start_uuid, end_uuid) + style_id = _wire_item_value(item, "wire_style_id") + if not style_id: continue - lane_key = _route_lane_key(start_uuid, end_uuid) - route_lane_index = lane_indexes_by_pair.get(lane_key, 0) - try: - endpoint_metadata = { - "start_element_uuid": _wire_item_value(item, "start_element_uuid"), - "start_terminal_display": _wire_item_value(item, "start_terminal_display"), - "start_device_label": _wire_item_value(item, "start_device_label"), - "end_element_uuid": _wire_item_value(item, "end_element_uuid"), - "end_terminal_display": _wire_item_value(item, "end_terminal_display"), - "end_device_label": _wire_item_value(item, "end_device_label"), - "endpoint_label": _wire_item_value(item, "endpoint_label"), - } - result = create_route( - route_lane_index, - dict(item, __segment_usage_costs=segment_usage_costs), - start_terminal, - end_terminal, - endpoint_metadata, - ) - route_segment_keys = _route_segment_keys(result) - shared_lane_index = max( - [lane_indexes_by_segment.get(key, 0) for key in route_segment_keys] or [0] - ) - final_lane_index = max(route_lane_index, shared_lane_index) - if final_lane_index != route_lane_index: - initial_wire = result.get("wire") if isinstance(result, dict) else None - try: - result = create_route( - final_lane_index, - dict(item, __segment_usage_costs=segment_usage_costs), - start_terminal, - end_terminal, - endpoint_metadata, - ) - except Exception: - if initial_wire is not None: - _remove_routing_connection_objects(doc, [initial_wire]) - raise - route_segment_keys = _route_segment_keys(result) - except Exception as exc: - error_text = str(exc) - if is_missing_route_network_error(error_text): - # 路径网络存在但两端无法连通时,按缺路径网络处理,避免被普通 Error 淹没。 - report["skipped_missing_route_network"] += 1 - add_status("MissingRouteNetwork") - set_item_task_status(item, "MissingRouteNetwork") - add_missing_route_network_sample(item, start_uuid, end_uuid, error_text=error_text) - continue - report["errors"].append(error_text) - add_status("Error") - set_item_task_status(item, "Error") - if len(report["error_samples"]) < 8: - report["error_samples"].append( + if style_id in resolved_ids: + summary["resolved"] += 1 + else: + summary["missing"] += 1 + if len(summary["missing_samples"]) < 8: + summary["missing_samples"].append( { "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), - "start_terminal_uuid": start_uuid, - "start_element_uuid": _wire_item_value(item, "start_element_uuid"), - "start_terminal_display": _wire_item_value(item, "start_terminal_display"), - "start_device_label": _wire_item_value(item, "start_device_label"), - "end_terminal_uuid": end_uuid, - "end_element_uuid": _wire_item_value(item, "end_element_uuid"), - "end_terminal_display": _wire_item_value(item, "end_terminal_display"), - "end_device_label": _wire_item_value(item, "end_device_label"), - "endpoint_label": _wire_item_value(item, "endpoint_label"), - "error": error_text, + "wire_style_id": style_id, } ) - continue - lane_indexes_by_pair[lane_key] = max( - lane_indexes_by_pair.get(lane_key, 0), - int(result.get("lane", {}).get("index", 0) or 0) + 1, + summary["missing_style_ids"] = sorted(missing_ids) + return summary + + +def _routing_boundary_summary(doc, options=None): + boundaries = collect_routing_boundaries(doc, options=options) + return { + "count": len(boundaries), + "samples": [ + { + "name": item.get("name", ""), + "label": item.get("label", ""), + "type_id": item.get("type_id", ""), + } + for item in boundaries[:8] + ], + } + + +def _routing_runtime_capabilities(): + route_constraint_collector = callable( + getattr(RoutingNetwork, "collect_route_constraint_options", None) + ) + return { + "ok": bool(route_constraint_collector), + "route_constraint_collector": bool(route_constraint_collector), + } + + +def _preflight_routeability_summary(doc, wires, terminals, options=None): + opts = options or {} + try: + sample_limit = max( + int(opts.get("preflight_routeability_sample_limit", DEFAULT_OPTIONS["preflight_routeability_sample_limit"]) or 0), + 0, ) - for segment_key in route_segment_keys: - lane_indexes_by_segment[segment_key] = max( - lane_indexes_by_segment.get(segment_key, 0), - int(result.get("lane", {}).get("index", 0) or 0) + 1, + except Exception: + sample_limit = int(DEFAULT_OPTIONS["preflight_routeability_sample_limit"]) + summary = { + "checked": 0, + "sample_limit": sample_limit, + "eligible_wires": 0, + "unchecked_wires": 0, + "unrouteable_wires": 0, + "unrouteable_samples": [], + } + if sample_limit <= 0: + return summary + for item in wires or []: + if not isinstance(item, dict): + continue + start_uuid = _wire_item_value(item, "start_terminal_uuid") + end_uuid = _wire_item_value(item, "end_terminal_uuid") + start_terminal = terminals.get(start_uuid) + end_terminal = terminals.get(end_uuid) + if start_terminal is None or end_terminal is None: + continue + summary["eligible_wires"] += 1 + if summary["checked"] >= sample_limit: + continue + summary["checked"] += 1 + try: + route_data = build_network_route( + start_terminal, + end_terminal, + route_index=0, + options=opts, + doc=doc, ) - segment_usage_costs[segment_key] = segment_usage_costs.get(segment_key, 0) + 1 - if result["route_status"] == "CollisionWarning": - report["collision_warnings"] += 1 - report["replaced_routed_connections"] += int( - result.get("replaced_routed_connections", 0) or 0 - ) - add_status(result["route_status"]) - route_collision_samples = [] - for collision in list(result.get("collisions", []) or [])[:3]: - sample = dict(collision) - sample.update( + except Exception as exc: + route_data = None + error_text = str(exc) + else: + error_text = "" + if route_data is not None: + continue + summary["unrouteable_wires"] += 1 + if len(summary["unrouteable_samples"]) < 8: + summary["unrouteable_samples"].append( { "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "wire_object_label": _wire_item_value(item, "wire_label", "wire_mark", "wire_id", "wire_uuid", "id"), "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "error": error_text or "无法在当前路径网络中连通两端。", } ) - route_collision_samples.append(sample) - if len(report["collision_samples"]) < 8: - report["collision_samples"].append(sample) - report["routed"] += 1 - route_length = float(result.get("length_mm", 0.0) or 0.0) - report["total_length_mm"] += route_length - report["routes"].append( - { + summary["unchecked_wires"] = max( + int(summary.get("eligible_wires", 0) or 0) - int(summary.get("checked", 0) or 0), + 0, + ) + return summary + + +def preflight_eplan_connections(doc, payload=None, options=None): + """Check whether a real QET project is ready for first-version auto routing.""" + if doc is None: + raise AutoRoutingError("No FreeCAD document is available.") + opts = _merged_options(options) + source_payload, wires, source = _preflight_wire_payload(doc, payload) + _apply_wire_style_database_option(opts, source_payload) + opts.setdefault("__wire_style_cache", {}) + project_uuid = str(source_payload.get("project_uuid", "") or _project_uuid(doc)).strip() + terminals = index_terminals(doc) + local_terminal_count = sum( + 1 + for terminal_uuid in terminals + if TerminalObjects.is_local_terminal_uuid(terminal_uuid) + ) + report = { + "ok": True, + "source": source, + "runtime_version": AUTO_ROUTING_RUNTIME_VERSION, + "project_uuid": project_uuid, + "total_wires": len(wires), + "available_terminals": len(terminals), + "local_terminals": local_terminal_count, + "route_network_carriers": 0, + "route_network_segments": 0, + "route_network_nodes": 0, + "batch_network_entry_candidate_limit": int( + opts.get("batch_network_entry_candidate_limit", 0) or 0 + ), + "batch_network_entry_total_candidate_limit": int( + opts.get("batch_network_entry_total_candidate_limit", 0) or 0 + ), + "missing_route_retry_candidate_limit": int( + opts.get("missing_route_retry_candidate_limit", 0) or 0 + ), + "missing_route_retries": 0, + "batch_avoid_obstacles": bool(opts.get("batch_avoid_obstacles", False)), + "missing_endpoint_uuids": [], + "missing_endpoint_samples": [], + "routeability_checked": 0, + "routeability_sample_limit": int( + opts.get("preflight_routeability_sample_limit", DEFAULT_OPTIONS["preflight_routeability_sample_limit"]) or 0 + ), + "routeability_eligible_wires": 0, + "routeability_unchecked_wires": 0, + "unrouteable_wires": 0, + "unrouteable_samples": [], + "routing_sources": {}, + "routing_boundaries": {}, + "routing_obstacle_modes": {}, + "routing_path_network_diagnostic": {}, + "runtime_capabilities": _routing_runtime_capabilities(), + "wire_style_database": {}, + "wire_style_database_fallback_from": str(opts.get("wire_style_database_fallback_from", "") or "").strip(), + "wire_style": {}, + "issues": [], + "issue_codes": [], + } + try: + network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + report["route_network_carriers"] = int(network.get("carrier_count", 0) or 0) + report["route_network_segments"] = int(network.get("segment_count", 0) or 0) + report["route_network_nodes"] = len(network.get("nodes", {}) or {}) + except Exception as exc: + report["route_network_error"] = str(exc) + _append_preflight_issue( + report, + "route_network_error", + "布线路径网络构建失败。", + severity="error", + ) + try: + report["routing_sources"] = RoutingNetwork.routing_source_summary(doc) + except Exception as exc: + report["routing_sources"] = {"error": str(exc)} + try: + report["routing_boundaries"] = _routing_boundary_summary(doc, options=opts) + except Exception as exc: + report["routing_boundaries"] = {"count": 0, "error": str(exc), "samples": []} + try: + report["routing_obstacle_modes"] = routing_obstacle_mode_summary(doc) + except Exception as exc: + report["routing_obstacle_modes"] = {"error": str(exc)} + try: + path_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), + ) + report["routing_path_network_diagnostic"] = _compact_routing_path_network_diagnostic(path_diagnostic) + _append_preflight_path_network_issues(report, report["routing_path_network_diagnostic"]) + except Exception as exc: + report["routing_path_network_diagnostic"] = { + "ok": False, + "issue_count": 1, + "issue_codes": ["routing_path_network_diagnostic_error"], + "issues": [ + { + "severity": "warning", + "code": "routing_path_network_diagnostic_error", + "count": 1, + } + ], + "error": str(exc), + } + _append_preflight_issue( + report, + "routing_path_network_diagnostic_error", + "路径网络诊断失败。", + severity="warning", + ) + + missing_endpoint_uuids = set() + for item in wires: + if not isinstance(item, dict): + continue + start_uuid = _wire_item_value(item, "start_terminal_uuid") + end_uuid = _wire_item_value(item, "end_terminal_uuid") + start_found = bool(start_uuid and start_uuid in terminals) + end_found = bool(end_uuid and end_uuid in terminals) + if start_found and end_found: + continue + for terminal_uuid, found in ((start_uuid, start_found), (end_uuid, end_found)): + if terminal_uuid and not found: + missing_endpoint_uuids.add(terminal_uuid) + if len(report["missing_endpoint_samples"]) < 8: + sample = { "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), - "wire_style_id": _wire_item_value(item, "wire_style_id"), + # 预检阶段同样没有 3D 导线对象,这里记录最接近对象标题的任务显示名。 + "wire_object_label": _wire_item_value(item, "wire_label", "wire_mark", "wire_id", "wire_uuid", "id"), "start_terminal_uuid": start_uuid, + "start_found": start_found, "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_device_label": _wire_item_value(item, "start_device_label"), "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_device_label": _wire_item_value(item, "end_device_label"), - "endpoint_label": _wire_item_value(item, "endpoint_label"), - "algorithm": result["algorithm"], - "route_status": result["route_status"], - "length_mm": route_length, - "lane": result.get("lane", {}), - "network": result.get("network", {}), - "route_track": result.get("route_track", {}), - "collision_count": result["collision_count"], - "collision_samples": route_collision_samples, } - ) - if report["routed"] > 0: - try: - doc.recompute() - except Exception: - pass + if not start_found: + _add_missing_endpoint_terminal_context(sample, "start", terminals, doc=doc) + if not end_found: + _add_missing_endpoint_terminal_context(sample, "end", terminals, doc=doc) + report["missing_endpoint_samples"].append(sample) report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) - _write_routing_connection_batch_diagnostic(doc, report) - return report + if report["route_network_segments"] > 0: + routeability = _preflight_routeability_summary(doc, wires, terminals, options=opts) + report["routeability_checked"] = int(routeability.get("checked", 0) or 0) + report["routeability_sample_limit"] = int(routeability.get("sample_limit", 0) or 0) + report["routeability_eligible_wires"] = int(routeability.get("eligible_wires", 0) or 0) + report["routeability_unchecked_wires"] = int(routeability.get("unchecked_wires", 0) or 0) + report["unrouteable_wires"] = int(routeability.get("unrouteable_wires", 0) or 0) + report["unrouteable_samples"] = list(routeability.get("unrouteable_samples", []) or []) + + database_status = _wire_style_database_status( + opts.get("wire_style_database_path", "") or os.environ.get("QET_WIRE_PROPERTIES_DB", "") + ) + report["wire_style_database"] = database_status + report["wire_style"] = _wire_style_preflight_summary( + wires, + opts, + project_uuid, + database_status, + ) -def _missing_endpoint_label(sample, side): - terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() - element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() - terminal_display = str(sample.get("{0}_terminal_display".format(side), "") or "").strip() - if element_uuid and terminal_display: - label = "{0}/{1}".format(element_uuid, terminal_display) - elif terminal_display: - label = terminal_display - elif element_uuid: - label = element_uuid - else: - return terminal_uuid - if terminal_uuid and terminal_uuid != label: - return "{0} ({1})".format(label, terminal_uuid) - return label + if report["total_wires"] <= 0: + _append_preflight_issue(report, "no_wire_tasks", "没有导线任务。", severity="error") + if report["available_terminals"] <= 0: + _append_preflight_issue(report, "no_available_terminals", "没有可用工程端子。", severity="error") + runtime_capabilities = report.get("runtime_capabilities", {}) + if isinstance(runtime_capabilities, dict) and not runtime_capabilities.get( + "route_constraint_collector", False + ): + _append_preflight_issue( + report, + "runtime_route_constraint_collector_missing", + "运行模块缺少路径约束收集函数,请同步 FreeCADExchange 运行目录并重启 FreeCAD。", + severity="error", + ) + if report["route_network_segments"] <= 0: + _append_preflight_issue( + report, + "no_route_network", + "没有可用布线路径网络。", + severity="error", + ) + routing_sources = report.get("routing_sources", {}) + if isinstance(routing_sources, dict): + candidate_sources = int(routing_sources.get("candidate_sources", 0) or 0) + route_carriers = int(routing_sources.get("route_carriers", 0) or 0) + if candidate_sources <= 0 and route_carriers <= 0: + _append_preflight_issue( + report, + "no_routing_sources", + "未识别到线槽、布线面或用户路径源;请先装配/标记线槽、安装板或草图路径。", + severity="error", + ) + elif route_carriers <= 0: + _append_preflight_issue( + report, + "routing_sources_not_generated", + "已识别到布线源,但还没有生成可用路径 carrier;请先生成布线路径网络。", + severity="error", + count=candidate_sources, + samples=routing_sources.get("candidate_samples", []), + ) + if report["missing_endpoint_uuids"]: + _append_preflight_issue( + report, + "missing_endpoints", + "部分导线端点没有匹配到 3D 工程端子。", + severity="error", + count=len(report["missing_endpoint_uuids"]), + samples=report["missing_endpoint_samples"], + ) + if report["unrouteable_wires"] > 0: + _append_preflight_issue( + report, + "unrouteable_wires", + "部分导线端点存在,但当前路径网络无法连通。", + severity="error", + count=report["unrouteable_wires"], + samples=report["unrouteable_samples"], + ) + if report["wire_style"].get("with_style_id", 0) > 0: + style_db_status = database_status.get("status", "") + if style_db_status == "NotConfigured": + _append_preflight_issue(report, "wire_style_database_not_configured", "导线样式库未配置。") + elif style_db_status == "Missing": + _append_preflight_issue(report, "wire_style_database_missing", "导线样式库文件不存在。") + elif style_db_status == "NoWirePropertiesTable": + _append_preflight_issue(report, "wire_style_database_no_table", "导线样式库缺少 wire_properties 表。") + elif style_db_status == "EmptyWirePropertiesTable": + _append_preflight_issue(report, "wire_style_database_empty", "导线样式库 wire_properties 表为空。") + elif style_db_status == "Unreadable": + _append_preflight_issue(report, "wire_style_database_unreadable", "导线样式库无法读取。") + if report["wire_style"].get("missing", 0) > 0: + _append_preflight_issue( + report, + "missing_wire_styles", + "部分导线样式 ID 无法在 wire_properties 中解析。", + count=report["wire_style"].get("missing", 0), + samples=report["wire_style"].get("missing_samples", []), + ) + if report["wire_style"].get("without_style_id", 0) > 0: + _append_preflight_issue( + report, + "wires_without_style_id", + "部分导线未带 wire_style_id,将使用默认显示样式。", + count=report["wire_style"].get("without_style_id", 0), + ) + report["ok"] = not bool(report["issues"]) + return report -def _missing_endpoint_side_summary(sample): - missing_sides = [] - if sample.get("start_found") is False: - missing_sides.append("起点") - if sample.get("end_found") is False: - missing_sides.append("终点") - if not missing_sides: - return "" - if len(missing_sides) == 2: - return "(缺失:两端)" - return "(缺失:{0})".format(missing_sides[0]) +def _wire_style_database_status_text(status): + labels = { + "Available": "可用", + "NotConfigured": "未配置", + "Missing": "文件不存在", + "NoWirePropertiesTable": "缺少 wire_properties 表", + "EmptyWirePropertiesTable": "wire_properties 为空", + "Unreadable": "无法读取", + } + return labels.get(str(status or "").strip(), str(status or "").strip() or "未知") -def _wire_sample_text(sample): - return ( - str(sample.get("wire_label", "") or "").strip() - or str(sample.get("wire_uuid", "") or "").strip() - or "未知导线" +def format_eplan_routing_preflight_report(report): + if not isinstance(report, dict): + return "布线准备度:无法读取预检报告。" + message = "布线准备度:{0}。".format("可执行" if report.get("ok") else "未通过") + source_label = { + "payload": "QET 会话交换数据", + "tasks": "当前 FreeCAD 文档任务", + }.get(str(report.get("source", "") or "").strip(), "") + if source_label: + message += "\n导线来源:{0}。".format(source_label) + runtime_version = str(report.get("runtime_version", "") or "").strip() + if runtime_version: + message += "\n运行版本:{0}。".format(runtime_version) + message += "\n导线任务:{0} 条;工程端子:{1} 个;本地端子:{2} 个。".format( + report.get("total_wires", 0), + report.get("available_terminals", 0), + report.get("local_terminals", 0), ) - - -def _endpoint_pair_text(sample): - endpoint_label = str(sample.get("endpoint_label", "") or "").strip() - if endpoint_label: - return endpoint_label - return "{0} -> {1}".format( - _missing_endpoint_label(sample, "start"), - _missing_endpoint_label(sample, "end"), + message += "\n路径网络:{0} 段({1} 条 carrier / {2} 节点)。".format( + report.get("route_network_segments", 0), + report.get("route_network_carriers", 0), + report.get("route_network_nodes", 0), ) - - -def _route_source_labels(route_track, limit=5): - labels = [] - seen = set() - if not isinstance(route_track, dict): - return labels - 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 - label = ( - str(carrier.get("source_label", "") or "").strip() - or str(carrier.get("source_name", "") or "").strip() + routing_sources = report.get("routing_sources", {}) + if isinstance(routing_sources, dict) and routing_sources: + candidate_sources = int(routing_sources.get("candidate_sources", 0) or 0) + if candidate_sources <= 0: + message += "\n布线源:未识别到线槽/布线面/用户路径。" + else: + message += "\n布线源:线槽 {0} 个,布线面 {1} 个,穿线孔 {2} 个,用户路径 {3} 个;已生成 carrier {4} 条。".format( + routing_sources.get("wire_duct_sources", 0), + routing_sources.get("support_surface_sources", 0), + routing_sources.get("wiring_cut_out_sources", 0), + routing_sources.get("user_path_sources", 0), + routing_sources.get("route_carriers", 0), + ) + routing_boundaries = report.get("routing_boundaries", {}) + if isinstance(routing_boundaries, dict) and routing_boundaries: + boundary_count = int(routing_boundaries.get("count", 0) or 0) + if boundary_count <= 0: + message += "\n柜内边界:未标记。" + else: + message += "\n柜内边界:{0} 个。".format(boundary_count) + routing_obstacle_modes = report.get("routing_obstacle_modes", {}) + if isinstance(routing_obstacle_modes, dict): + pass_through = routing_obstacle_modes.get("PassThrough", {}) + if isinstance(pass_through, dict): + pass_through_count = int(pass_through.get("count", 0) or 0) + if pass_through_count > 0: + message += "\n忽略碰撞对象:{0} 个。".format(pass_through_count) + 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 = [ + _routing_path_network_issue_label(code) + for code in list(path_diagnostic.get("issue_codes", []) or [])[:3] + ] + message += "\n路径网络检查提示:{0}。".format("、".join(issue_labels) if issue_labels else "存在问题") + outside_sources = _dict_items(path_diagnostic.get("route_carriers_outside_boundary", []) or []) + if outside_sources: + sample = outside_sources[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + carrier_text = carrier.get("label") or carrier.get("name") or "未知路径对象" + message += " 越界路径:{0} {1} 个越界点。".format( + carrier_text, + _safe_int(sample.get("outside_point_count", 0)), + ) + outside_terminals = _dict_items(path_diagnostic.get("terminals_outside_boundary", []) or []) + if outside_terminals: + sample = outside_terminals[0] + message += " 越界端子:{0} {1} 个越界点。".format( + _diagnostic_terminal_text(sample), + _safe_int(sample.get("outside_point_count", 0)), + ) + runtime_capabilities = report.get("runtime_capabilities", {}) + if isinstance(runtime_capabilities, dict) and not runtime_capabilities.get( + "route_constraint_collector", True + ): + message += "\n运行模块能力:路径约束收集函数缺失,请同步运行目录并重启 FreeCAD。" + database_status = report.get("wire_style_database", {}) + if isinstance(database_status, dict): + message += "\n导线样式库:{0}".format( + _wire_style_database_status_text(database_status.get("status", "")) ) - if not label or label in seen: - continue - seen.add(label) - labels.append(label) - if len(labels) >= int(limit or 0): - break - return labels - - -def _route_source_sample_text(report): - for route in report.get("routes", []) or []: - if not isinstance(route, dict): - continue - labels = _route_source_labels(route.get("route_track", {})) - if not labels: - continue - return "路径示例:导线 {0} 经过 {1}。".format( - _wire_sample_text(route), - "、".join(labels), + path = str(database_status.get("path", "") or "").strip() + if path: + message += ",{0}".format(path) + fallback_from = str(report.get("wire_style_database_fallback_from", "") or "").strip() + if fallback_from: + message += "(从备用库恢复,原库:{0})".format(fallback_from) + message += "。" + wire_style = report.get("wire_style", {}) + if isinstance(wire_style, dict): + parts = [] + for key, label in ( + ("resolved", "已解析"), + ("missing", "缺失样式"), + ("without_style_id", "未设置样式"), + ): + try: + value = int(wire_style.get(key, 0) or 0) + except Exception: + value = 0 + if value > 0: + parts.append("{0} {1} 条".format(label, value)) + if parts: + message += "\n导线样式:{0}。".format(",".join(parts)) + issues = [item for item in list(report.get("issues", []) or []) if isinstance(item, dict)] + if issues: + message += "\n预检问题:{0}。".format( + ";".join(str(item.get("message", "") or item.get("code", "")) for item in issues[:5]) ) - return "" - + missing_samples = list(report.get("missing_endpoint_samples", []) or []) + if missing_samples: + sample = missing_samples[0] + wire_text = _wire_object_sample_text(sample) + if wire_text and wire_text != "未知导线": + message += "\n端点缺失示例:导线 {0},{1}。".format( + wire_text, + _endpoint_pair_text(sample), + ) + else: + message += "\n端点缺失示例:{0}。".format(_endpoint_pair_text(sample)) + detail_text = _missing_endpoint_detail_text(sample) + if detail_text: + message += "\n端点缺失明细:{0}。".format(detail_text) + style_samples = [] + if isinstance(wire_style, dict): + style_samples = list(wire_style.get("missing_samples", []) or []) + if style_samples: + sample = style_samples[0] + message += "\n样式缺失示例:导线 {0},样式 {1}。".format( + _wire_sample_text(sample), + sample.get("wire_style_id", ""), + ) + routeability_checked = int(report.get("routeability_checked", 0) or 0) + routeability_unchecked = int(report.get("routeability_unchecked_wires", 0) or 0) + if routeability_checked > 0 or routeability_unchecked > 0: + message += "\n可达性抽样:已检查 {0} 条".format(routeability_checked) + if routeability_unchecked > 0: + message += ",未检查 {0} 条".format(routeability_unchecked) + message += "。" + unrouteable_samples = list(report.get("unrouteable_samples", []) or []) + if unrouteable_samples: + sample = unrouteable_samples[0] + message += "\n导线不可达示例:导线 {0},{1};原因:{2}".format( + _wire_sample_text(sample), + _endpoint_pair_text(sample), + sample.get("error", "当前路径网络无法连通。"), + ) + message += "。" + return message -def _route_network_metric_max(report, key): - maximum = 0 - for route in report.get("routes", []) or []: - if not isinstance(route, dict): - continue - if key == "bridged_segments": - route_track = route.get("route_track", {}) - if isinstance(route_track, dict) and key in route_track: - try: - maximum = max(maximum, int(route_track.get(key, 0) or 0)) - except Exception: - pass - continue - network = route.get("network", {}) - if not isinstance(network, dict): - continue - try: - maximum = max(maximum, int(network.get(key, 0) or 0)) - except Exception: - continue - return maximum +def route_eplan_connections_from_payload(doc, payload, options=None, prepared_layout=None): + if doc is None: + raise AutoRoutingError("No FreeCAD document is available.") + if not isinstance(payload, dict): + raise AutoRoutingError("Exchange payload must be an object.") -def _route_lane_summary(report): - max_lane_index = 0 - lane_spacing = 0.0 - lane_max_offset = 0.0 - for route in report.get("routes", []) or []: - if not isinstance(route, dict): - continue - lane = route.get("lane", {}) - if not isinstance(lane, dict): - continue - try: - lane_index = int(lane.get("index", 0) or 0) - except Exception: - lane_index = 0 - if lane_index <= max_lane_index: - continue - max_lane_index = lane_index - try: - lane_spacing = float(lane.get("spacing_mm", 0.0) or 0.0) - except Exception: - lane_spacing = 0.0 - try: - lane_max_offset = float(lane.get("max_offset_mm", 0.0) or 0.0) - except Exception: - lane_max_offset = 0.0 - if max_lane_index <= 0: + payload = _load_context_payload_with_wire_styles(payload) + payload = _load_context_payload_with_devices(payload) + opts = _merged_options(options) + _apply_wire_style_database_option(opts, payload) + opts.setdefault("__wire_style_cache", {}) + terminal_binding_report = bind_wire_task_terminals_from_payload(doc, payload) + terminals = index_terminals(doc) + local_terminal_count = sum( + 1 + for terminal_uuid in terminals + if TerminalObjects.is_local_terminal_uuid(terminal_uuid) + ) + wires = payload.get("wires", []) or [] + payload_devices = _payload_device_index(payload) + project_uuid_value = str(payload.get("project_uuid", "") or _project_uuid(doc)).strip() + report = { + "project_uuid": project_uuid_value, + "runtime_version": AUTO_ROUTING_RUNTIME_VERSION, + "total_wires": len(wires), + "available_terminals": len(terminals), + "local_terminals": local_terminal_count, + "auto_bound_terminals": terminal_binding_report["bound"], + "auto_created_terminals": terminal_binding_report["created"], + "auto_terminal_binding_warnings": terminal_binding_report["warnings"], + "routed": 0, + "collision_warnings": 0, + "replaced_routed_connections": 0, + "total_length_mm": 0.0, + "skipped_missing_terminal": 0, + "skipped_missing_route_network": 0, + "skipped_invalid": 0, + "terminal_access_warning_distance": float( + opts.get( + "terminal_access_warning_distance", + RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE, + ) + or 0.0 + ), + "batch_network_entry_candidate_limit": int( + opts.get("batch_network_entry_candidate_limit", 0) or 0 + ), + "batch_network_entry_total_candidate_limit": int( + opts.get("batch_network_entry_total_candidate_limit", 0) or 0 + ), + "missing_route_retry_candidate_limit": int( + opts.get("missing_route_retry_candidate_limit", 0) or 0 + ), + "missing_route_retries": 0, + "batch_avoid_obstacles": bool(opts.get("batch_avoid_obstacles", False)), + "selective_collision_reroute": bool(opts.get("selective_collision_reroute", True)), + "selective_collision_reroute_limit": int( + opts.get("selective_collision_reroute_limit", 0) or 0 + ), + "selective_collision_reroute_allow_fallback": bool( + opts.get("selective_collision_reroute_allow_fallback", False) + ), + "selective_collision_reroute_attempts": 0, + "selective_collision_reroutes": 0, + "selective_collision_reroute_no_improvement": 0, + "selective_collision_reroute_rejected_fallback": 0, + "selective_collision_reroute_errors": 0, + "missing_endpoint_uuids": [], + "missing_endpoint_samples": [], + "missing_route_network_samples": [], + "collision_samples": [], + "errors": [], + "error_samples": [], + "route_status_counts": {}, + "wire_style_status_counts": {}, + "wire_style_database_path": str(opts.get("wire_style_database_path", "") or "").strip(), + "wire_style_database_fallback_from": str(opts.get("wire_style_database_fallback_from", "") or "").strip(), + "context_devices_loaded": bool(str(payload.get("__context_devices_json_path", "") or "").strip()), + "context_device_count": _safe_int(payload.get("__context_device_count", 0)), + "context_devices_json_path": str(payload.get("__context_devices_json_path", "") or "").strip(), + "routing_sources": {}, + "routes": [], + } + if isinstance(prepared_layout, dict): + report["prepared_layout"] = prepared_layout + try: + route_network = RoutingNetwork.build_route_graph( + doc, + adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0), + ) + except Exception as exc: + route_network = {} + report["route_network_error"] = str(exc) + report["route_network_carriers"] = int(route_network.get("carrier_count", 0) or 0) + report["route_network_segments"] = int(route_network.get("segment_count", 0) or 0) + report["route_network_nodes"] = len(route_network.get("nodes", {}) or {}) + report["route_network_carrier_kind_counts"] = _route_network_carrier_kind_counts(route_network) + try: + report["routing_sources"] = RoutingNetwork.routing_source_summary(doc) + except Exception as exc: + report["routing_sources"] = {"error": str(exc)} + has_route_network = report["route_network_segments"] > 0 + obstacle_candidate_cache = _obstacle_candidate_cache(doc, options=opts) + report["batch_obstacle_candidates"] = len( + obstacle_candidate_cache.get("candidates", []) or [] + ) + missing_endpoint_uuids = set() + segment_usage_costs = _existing_routed_segment_usage( + doc, + excluded_wire_uuids=_incoming_wire_uuids(wires), + ) + lane_indexes_by_pair = {} + # 已存在的 RoutedConnection 也要占用显示 lane;否则增量布线时新线会从 lane 0 开始贴到旧线上。 + lane_indexes_by_segment = { + segment_key: max(int(usage_count or 0), 0) + for segment_key, usage_count in segment_usage_costs.items() + if int(usage_count or 0) > 0 + } + + def add_status(status): + key = str(status or "").strip() or "Unknown" + report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 + + def add_wire_style_status(status): + key = str(status or "").strip() + if not key: + return + report["wire_style_status_counts"][key] = ( + report["wire_style_status_counts"].get(key, 0) + 1 + ) + + def set_item_task_status(item, status): + wire_uuid = _wire_item_value(item, "wire_id", "wire_uuid", "id") + if not wire_uuid: + return + _set_task_status(_find_task_by_wire_uuid(doc, wire_uuid), status) + + def missing_route_network_sample(item, start_uuid, end_uuid, error_text=""): + start_payload_device = _payload_device_for_endpoint(payload_devices, item, "start") + end_payload_device = _payload_device_for_endpoint(payload_devices, item, "end") + sample = { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + # 失败样例没有真实导线对象,这里保留任务侧最接近对象标题的显示名,方便手工复盘。 + "wire_object_label": _wire_item_value(item, "wire_label", "wire_mark", "wire_id", "wire_uuid", "id"), + "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label", "start_display_tag") + or _payload_device_value(start_payload_device, "display_tag", "label", "name"), + "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label", "end_display_tag") + or _payload_device_value(end_payload_device, "display_tag", "label", "name"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + } + if error_text: + sample["error"] = error_text + return sample + + def add_missing_route_network_sample(item, start_uuid, end_uuid, error_text=""): + if len(report["missing_route_network_samples"]) >= 8: + return + report["missing_route_network_samples"].append( + missing_route_network_sample(item, start_uuid, end_uuid, error_text=error_text) + ) + + def is_missing_route_network_error(error_text): + text = str(error_text or "") + return ( + "没有可用的布线路径网络" in text + or "没有满足路径约束的布线路径网络" in text + or "No route path" in text + ) + + def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): + route_options = dict(opts) + route_options["avoid_obstacles"] = bool(opts.get("batch_avoid_obstacles", False)) + if isinstance(item, dict) and "__avoid_obstacles_override" in item: + route_options["avoid_obstacles"] = bool(item.get("__avoid_obstacles_override")) + if isinstance(item, dict) and "__replace_existing_override" in item: + route_options["replace_existing"] = bool(item.get("__replace_existing_override")) + if isinstance(route_network, dict) and route_network.get("segment_count", 0) > 0: + route_options["__base_route_network"] = route_network + route_options["__obstacle_candidate_cache"] = obstacle_candidate_cache + batch_candidate_limit = int(opts.get("batch_network_entry_candidate_limit", 0) or 0) + batch_total_candidate_limit = int(opts.get("batch_network_entry_total_candidate_limit", 0) or 0) + override_candidate_limit = 0 + if isinstance(item, dict): + override_candidate_limit = int(item.get("__network_entry_candidate_limit_override", 0) or 0) + if override_candidate_limit > 0: + route_options["network_entry_candidate_limit"] = override_candidate_limit + route_options["network_entry_candidate_total_limit"] = max( + int(route_options.get("network_entry_candidate_total_limit", 0) or 0), + override_candidate_limit, + ) + elif batch_candidate_limit > 0: + current_candidate_limit = int( + route_options.get("network_entry_candidate_limit", batch_candidate_limit) + or batch_candidate_limit + ) + route_options["network_entry_candidate_limit"] = min( + current_candidate_limit, + batch_candidate_limit, + ) + if batch_total_candidate_limit > 0 and int(route_options.get("network_entry_candidate_total_limit", 0) or 0) <= 0: + route_options["network_entry_candidate_total_limit"] = max( + batch_total_candidate_limit, + route_options["network_entry_candidate_limit"], + ) + if isinstance(item, dict) and "__segment_usage_costs" in item: + route_options["segment_usage_costs"] = item.get("__segment_usage_costs", {}) + if isinstance(item, dict): + for key in ( + "forbidden_route_carrier_names", + "forbidden_route_carrier_labels", + "forbidden_route_carrier_source_names", + "forbidden_route_carrier_source_labels", + "forbidden_route_carrier_kinds", + "required_route_carrier_names", + "required_route_carrier_labels", + "required_route_carrier_source_names", + "required_route_carrier_source_labels", + "required_route_carrier_kinds", + ): + if key in item: + route_options[key] = item.get(key) + return route_eplan_connection_between_terminals( + doc, + start_terminal, + end_terminal, + route_index=route_lane_index, + options=route_options, + wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), + wire_label=_wire_item_value(item, "wire_label", "wire_mark"), + net_uuid=_wire_item_value(item, "net_uuid"), + group_uuid=_wire_item_value(item, "group_uuid"), + wire_mark=_wire_item_value(item, "wire_mark"), + wire_mark_is_manual=bool(item.get("wire_mark_is_manual", False)), + wire_style_id=_wire_item_value(item, "wire_style_id"), + endpoint_metadata=endpoint_metadata, + defer_recompute=True, + ) + + for item in wires: + if not isinstance(item, dict): + report["skipped_invalid"] += 1 + add_status("Invalid") + continue + start_uuid = _wire_item_value(item, "start_terminal_uuid") + end_uuid = _wire_item_value(item, "end_terminal_uuid") + start_terminal = terminals.get(start_uuid) + end_terminal = terminals.get(end_uuid) + if 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: + missing_endpoint_uuids.add(terminal_uuid) + # 这里只保留少量样例,避免面板状态被大量导线任务刷屏。 + if len(report["missing_endpoint_samples"]) < 8: + start_payload_device = _payload_device_for_endpoint(payload_devices, item, "start") + end_payload_device = _payload_device_for_endpoint(payload_devices, item, "end") + sample = { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + # 这里还没生成 3D 导线对象,保留最接近对象标题的任务显示名。 + "wire_object_label": _wire_item_value(item, "wire_label", "wire_mark", "wire_id", "wire_uuid", "id"), + "start_terminal_uuid": start_uuid, + "start_found": start_terminal is not None, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_instance_id": _wire_item_value(item, "start_instance_id") + or _payload_device_value(start_payload_device, "instance_id"), + "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"), + "end_terminal_uuid": end_uuid, + "end_found": end_terminal is not None, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_instance_id": _wire_item_value(item, "end_instance_id") + or _payload_device_value(end_payload_device, "instance_id"), + "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"), + } + if start_terminal is None: + _add_missing_endpoint_terminal_context(sample, "start", terminals, doc=doc) + if end_terminal is None: + _add_missing_endpoint_terminal_context(sample, "end", terminals, doc=doc) + report["missing_endpoint_samples"].append(sample) + continue + if not has_route_network: + report["skipped_missing_route_network"] += 1 + add_status("MissingRouteNetwork") + set_item_task_status(item, "MissingRouteNetwork") + add_missing_route_network_sample(item, start_uuid, end_uuid) + continue + lane_key = _route_lane_key(start_uuid, end_uuid) + route_lane_index = lane_indexes_by_pair.get(lane_key, 0) + result = None + route_segment_keys = [] + try: + start_payload_device = _payload_device_for_endpoint(payload_devices, item, "start") + end_payload_device = _payload_device_for_endpoint(payload_devices, item, "end") + endpoint_metadata = { + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label", "start_display_tag") + or _payload_device_value(start_payload_device, "display_tag", "label", "name"), + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label", "end_display_tag") + or _payload_device_value(end_payload_device, "display_tag", "label", "name"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + } + result = create_route( + route_lane_index, + dict(item, __segment_usage_costs=segment_usage_costs), + start_terminal, + end_terminal, + endpoint_metadata, + ) + route_segment_keys = _route_segment_keys(result) + shared_lane_index = max( + [lane_indexes_by_segment.get(key, 0) for key in route_segment_keys] or [0] + ) + final_lane_index = max(route_lane_index, shared_lane_index) + if final_lane_index != route_lane_index: + initial_wire = result.get("wire") if isinstance(result, dict) else None + try: + result = create_route( + final_lane_index, + dict(item, __segment_usage_costs=segment_usage_costs), + start_terminal, + end_terminal, + endpoint_metadata, + ) + except Exception: + if initial_wire is not None: + _remove_routing_connection_objects(doc, [initial_wire]) + raise + route_segment_keys = _route_segment_keys(result) + selective_limit = int(opts.get("selective_collision_reroute_limit", 0) or 0) + selective_attempts = int(report.get("selective_collision_reroute_attempts", 0) or 0) + if ( + bool(opts.get("selective_collision_reroute", True)) + and not bool(opts.get("batch_avoid_obstacles", False)) + and selective_attempts < selective_limit + and _result_third_party_collision_count(result, item) > 0 + ): + report["selective_collision_reroute_attempts"] += 1 + original_result = result + original_wire = result.get("wire") if isinstance(result, dict) else None + original_collision_count = _result_collision_count(result) + try: + retry_result = create_route( + int(result.get("lane", {}).get("index", final_lane_index) or final_lane_index), + dict( + item, + __segment_usage_costs=segment_usage_costs, + __avoid_obstacles_override=True, + __replace_existing_override=False, + ), + start_terminal, + end_terminal, + endpoint_metadata, + ) + retry_collision_count = _result_collision_count(retry_result) + retry_quality = _route_quality_payload(retry_result.get("route_track", {})) + retry_uses_fallback = ( + retry_quality.get("quality_status") == "FallbackPathWarning" + ) + allow_fallback = bool( + opts.get("selective_collision_reroute_allow_fallback", False) + ) + if retry_collision_count < original_collision_count and ( + allow_fallback or not retry_uses_fallback + ): + if original_wire is not None: + _remove_routing_connection_objects(doc, [original_wire]) + result = retry_result + route_segment_keys = _route_segment_keys(result) + report["selective_collision_reroutes"] += 1 + else: + retry_wire = retry_result.get("wire") if isinstance(retry_result, dict) else None + if retry_wire is not None: + _remove_routing_connection_objects(doc, [retry_wire]) + result = original_result + if retry_uses_fallback and not allow_fallback: + detour_path = _create_main_path_detour_user_path_from_retry( + doc, + retry_result, + original_result, + project_uuid=project_uuid_value, + ) + result["selective_collision_reroute_status"] = "RejectedFallback" + result["selective_collision_reroute_rejected_fallback"] = True + result["selective_collision_reroute_rejected_fallback_kinds"] = list( + retry_quality.get("fallback_carrier_kinds", []) or [] + ) + result["selective_collision_reroute_rejected_fallback_labels"] = list( + retry_quality.get("fallback_carrier_labels", []) or [] + ) + if detour_path is not None: + result["auto_main_path_detour_user_path"] = getattr(detour_path, "Name", "") + report["selective_collision_reroute_rejected_fallback"] += 1 + else: + result["selective_collision_reroute_status"] = "NoImprovement" + report["selective_collision_reroute_no_improvement"] += 1 + if original_wire is not None: + _set_routing_connection_metadata( + original_wire, + result, + result.get("collisions", []), + wire_style_id=_wire_item_value(item, "wire_style_id"), + endpoint_metadata=endpoint_metadata, + wire_style=result.get("wire_style", {}), + ) + _set_task_status(_find_task_by_wire_uuid(doc, _wire_item_value(item, "wire_id", "wire_uuid", "id")), result["route_status"]) + except Exception: + report["selective_collision_reroute_errors"] += 1 + result = original_result + _set_task_status(_find_task_by_wire_uuid(doc, _wire_item_value(item, "wire_id", "wire_uuid", "id")), result["route_status"]) + except Exception as exc: + error_text = str(exc) + if is_missing_route_network_error(error_text): + retry_limit = int(opts.get("missing_route_retry_candidate_limit", 0) or 0) + active_limit = int(opts.get("batch_network_entry_candidate_limit", 0) or 0) + retry_succeeded = False + if retry_limit > max(active_limit, 0): + try: + result = create_route( + route_lane_index, + dict( + item, + __segment_usage_costs=segment_usage_costs, + __network_entry_candidate_limit_override=retry_limit, + ), + start_terminal, + end_terminal, + endpoint_metadata, + ) + route_segment_keys = _route_segment_keys(result) + report["missing_route_retries"] += 1 + retry_succeeded = True + except Exception as retry_exc: + error_text = str(retry_exc) + if retry_succeeded: + pass + else: + # 路径网络存在但两端无法连通时,按缺路径网络处理,避免被普通 Error 淹没。 + report["skipped_missing_route_network"] += 1 + add_status("MissingRouteNetwork") + set_item_task_status(item, "MissingRouteNetwork") + add_missing_route_network_sample(item, start_uuid, end_uuid, error_text=error_text) + continue + else: + report["errors"].append(error_text) + add_status("Error") + set_item_task_status(item, "Error") + if len(report["error_samples"]) < 8: + report["error_samples"].append( + { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + # 这个样例对应的是任务记录,不一定已经生成 FreeCAD 导线对象。 + "wire_object_label": _wire_item_value(item, "wire_label", "wire_mark", "wire_id", "wire_uuid", "id"), + "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "start_terminal_display": _wire_item_value(item, "start_terminal_display"), + "start_device_label": _wire_item_value(item, "start_device_label"), + "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "end_terminal_display": _wire_item_value(item, "end_terminal_display"), + "end_device_label": _wire_item_value(item, "end_device_label"), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + "error": error_text, + } + ) + continue + lane_indexes_by_pair[lane_key] = max( + lane_indexes_by_pair.get(lane_key, 0), + int(result.get("lane", {}).get("index", 0) or 0) + 1, + ) + for segment_key in route_segment_keys: + lane_indexes_by_segment[segment_key] = max( + lane_indexes_by_segment.get(segment_key, 0), + int(result.get("lane", {}).get("index", 0) or 0) + 1, + ) + segment_usage_costs[segment_key] = segment_usage_costs.get(segment_key, 0) + 1 + if result["route_status"] == "CollisionWarning": + report["collision_warnings"] += 1 + report["replaced_routed_connections"] += int( + result.get("replaced_routed_connections", 0) or 0 + ) + add_status(result["route_status"]) + add_wire_style_status(result.get("wire_style_status", "")) + route_collision_samples = [] + route_source_labels = _route_source_labels(result.get("route_track", {}), limit=4) + for collision in list(result.get("collisions", []) or [])[:3]: + sample = dict(collision) + sample.update( + { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "wire_object_label": result.get("wire_object_label", ""), + "start_terminal_uuid": start_uuid, + "start_element_uuid": _wire_item_value(item, "start_element_uuid"), + "end_terminal_uuid": end_uuid, + "end_element_uuid": _wire_item_value(item, "end_element_uuid"), + "route_source_labels": route_source_labels, + } + ) + sample["collision_relation"] = _collision_relation(sample) + route_collision_samples.append(sample) + if len(report["collision_samples"]) < 8: + report["collision_samples"].append(sample) + report["routed"] += 1 + route_length = float(result.get("length_mm", 0.0) or 0.0) + report["total_length_mm"] += route_length + route_record = { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "wire_object_label": result.get("wire_object_label", ""), + "wire_style_id": _wire_item_value(item, "wire_style_id"), + "wire_style_status": result.get("wire_style_status", ""), + "start_terminal_uuid": start_uuid, + "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", ""), + "end_terminal_uuid": end_uuid, + "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", ""), + "endpoint_label": _wire_item_value(item, "endpoint_label"), + "algorithm": result["algorithm"], + "route_status": result["route_status"], + "length_mm": route_length, + "lane": result.get("lane", {}), + "network": result.get("network", {}), + "route_track": result.get("route_track", {}), + "collision_count": result["collision_count"], + "collisions": route_collision_samples, + "collision_samples": route_collision_samples, + } + selective_status = str(result.get("selective_collision_reroute_status", "") or "").strip() + if selective_status: + route_record["selective_collision_reroute_status"] = selective_status + route_record["selective_collision_reroute_rejected_fallback_kinds"] = list( + result.get("selective_collision_reroute_rejected_fallback_kinds", []) or [] + ) + route_record["selective_collision_reroute_rejected_fallback_labels"] = list( + result.get("selective_collision_reroute_rejected_fallback_labels", []) or [] + ) + if str(result.get("auto_main_path_detour_user_path", "") or "").strip(): + route_record["auto_main_path_detour_user_path"] = str( + result.get("auto_main_path_detour_user_path", "") or "" + ).strip() + route_record["issue_codes"] = _route_issue_codes(route_record, route_collision_samples) + route_record["issue_labels"] = [ + _routing_diagnostic_issue_label(code) + for code in route_record["issue_codes"] + ] + if isinstance(result.get("wire_style"), dict) and result.get("wire_style"): + route_record["wire_style"] = dict(result.get("wire_style") or {}) + report["routes"].append(route_record) + if report["routed"] > 0: + try: + doc.recompute() + except Exception: + pass + report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) + missing_terminal_summary = _batch_missing_terminal_summary(report, doc=doc) + if _safe_int(missing_terminal_summary.get("skipped_missing_terminal", 0)) > 0: + # 原始 report 也保留结构化缺端子分组,便于面板和调试脚本不用再解析中文文本。 + report["missing_terminal_summary"] = missing_terminal_summary + _raise_main_path_detour_capacities_from_report(doc, report) + report["route_path_usage"] = _route_path_usage_summary(report) + report["top_collision_obstacles"] = _top_collision_obstacles(report) + _attach_routing_path_network_diagnostic_if_needed(doc, report, opts) + report["issue_codes"] = _routing_connection_batch_issue_codes(report) + _attach_main_path_detour_report_summary(doc, report) + _write_routing_connection_batch_diagnostic(doc, report) + return report + + +def _missing_endpoint_label(sample, side): + terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() + device_label = str(sample.get("{0}_device_label".format(side), "") or "").strip() + terminal_display = str(sample.get("{0}_terminal_display".format(side), "") or "").strip() + if device_label and terminal_display: + label = "{0}/{1}".format(device_label, terminal_display) + elif device_label: + label = device_label + elif element_uuid and terminal_display: + label = "{0}/{1}".format(element_uuid, terminal_display) + elif terminal_display: + label = terminal_display + elif element_uuid: + label = element_uuid + else: + return terminal_uuid + if terminal_uuid and terminal_uuid != label: + return "{0} ({1})".format(label, terminal_uuid) + return label + + +def _missing_endpoint_side_summary(sample): + missing_sides = [] + if sample.get("start_found") is False: + missing_sides.append("起点") + if sample.get("end_found") is False: + missing_sides.append("终点") + if not missing_sides: + return "" + if len(missing_sides) == 2: + return "(缺失:两端)" + return "(缺失:{0})".format(missing_sides[0]) + + +def _wire_sample_text(sample): + return ( + str(sample.get("wire", "") or "").strip() + or str(sample.get("wire_label", "") or "").strip() + or str(sample.get("wire_uuid", "") or "").strip() + or "未知导线" + ) + + +def _wire_object_sample_text(sample): + # 需要用户回到 FreeCAD 树目录定位对象时,优先显示对象 Label;普通统计仍用导线号保持简洁。 + return str(sample.get("wire_object_label", "") or "").strip() or _wire_sample_text(sample) + + +def _endpoint_pair_text(sample): + endpoint_label = str(sample.get("endpoint_label", "") or "").strip() + if endpoint_label: + return endpoint_label + return "{0} -> {1}".format( + _missing_endpoint_label(sample, "start"), + _missing_endpoint_label(sample, "end"), + ) + + +def _missing_endpoint_detail_text(sample): + if not isinstance(sample, dict): + return "" + parts = [] + for side, label in (("start", "起点"), ("end", "终点")): + if sample.get("{0}_found".format(side)) is not False: + continue + element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() + instance_id = str(sample.get("{0}_instance_id".format(side), "") or "").strip() + device_label = str(sample.get("{0}_device_label".format(side), "") or "").strip() + terminal_display = str(sample.get("{0}_terminal_display".format(side), "") or "").strip() + terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + fields = [] + if device_label: + fields.append("device={0}".format(device_label)) + if element_uuid: + fields.append("element={0}".format(element_uuid)) + if instance_id: + fields.append("instance={0}".format(instance_id)) + if terminal_display: + fields.append("terminal={0}".format(terminal_display)) + if terminal_uuid: + fields.append("uuid={0}".format(terminal_uuid)) + if "{0}_element_terminal_count".format(side) in sample: + fields.append( + "FreeCAD同设备端子={0}".format( + _safe_int(sample.get("{0}_element_terminal_count".format(side), 0)) + ) + ) + if "{0}_instance_terminal_count".format(side) in sample: + fields.append( + "FreeCAD同实例端子={0}".format( + _safe_int(sample.get("{0}_instance_terminal_count".format(side), 0)) + ) + ) + reason_label = str(sample.get("{0}_missing_endpoint_reason_label".format(side), "") or "").strip() + if reason_label: + fields.append("原因={0}".format(reason_label)) + if fields: + parts.append("{0} {1}".format(label, ", ".join(fields))) + return ";".join(parts) + + +def _missing_endpoint_reason_counts_from_samples(samples): + counts = {} + for sample in list(samples or []): + if not isinstance(sample, dict): + continue + for side in ("start", "end"): + if sample.get("{0}_found".format(side)) is not False: + continue + reason_code = str(sample.get("{0}_missing_endpoint_reason_code".format(side), "") or "").strip() + if not reason_code: + continue + counts[reason_code] = counts.get(reason_code, 0) + 1 + return counts + + +def _missing_endpoint_reason_hint_text(reason_counts): + if not isinstance(reason_counts, dict): + return "" + parts = [] + if _safe_int(reason_counts.get("missing_device_binding_metadata", 0)) > 0: + parts.append( + "QET 导线端点缺少 element_uuid,FreeCAD 无法判断缺失端子属于哪个 2D 设备;" + "第一版不要求 start/end_instance_id" + ) + if _safe_int(reason_counts.get("device_not_in_3d_scene", 0)) > 0: + parts.append("部分导线引用的设备未在当前 FreeCAD 场景中找到,请先检查设备导入、装配和 2D/3D 绑定") + if not parts: + return "" + return ";".join(parts) + "。" + + +def _route_source_labels(route_track, limit=5): + labels = [] + seen = set() + if not isinstance(route_track, dict): + return labels + 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 + label = ( + str(carrier.get("source_label", "") or "").strip() + or str(carrier.get("source_name", "") or "").strip() + or str(carrier.get("label", "") or "").strip() + or str(carrier.get("name", "") or "").strip() + ) + 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 not label or label in seen: + continue + seen.add(label) + labels.append(label) + if len(labels) >= int(limit or 0): + break + return labels + + +def _route_source_sample_text(report): + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + labels = _route_source_labels(route.get("route_track", {})) + if not labels: + continue + return "路径示例:导线 {0} 经过 {1}。".format( + _wire_sample_text(route), + "、".join(labels), + ) + return "" + + +def _route_warning_carrier_labels(route_track, warning_kinds, limit=4): + labels = [] + seen = set() + if not isinstance(route_track, dict): + return labels + warning_kind_set = { + str(kind or "").strip() + for kind in (warning_kinds or []) + if str(kind or "").strip() + } + 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 + kind = str(carrier.get("kind", "") or "").strip() + if kind not in warning_kind_set: + continue + label = ( + str(carrier.get("source_label", "") or "").strip() + or str(carrier.get("source_name", "") or "").strip() + or str(carrier.get("label", "") or "").strip() + or str(carrier.get("name", "") or "").strip() + ) + if not label or label in seen: + continue + seen.add(label) + labels.append(label) + if len(labels) >= int(limit or 0): + break + return labels + + +def _route_network_metric_max(report, key): + maximum = 0 + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + if key == "bridged_segments": + route_track = route.get("route_track", {}) + if isinstance(route_track, dict) and key in route_track: + try: + maximum = max(maximum, int(route_track.get(key, 0) or 0)) + except Exception: + pass + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + try: + maximum = max(maximum, int(network.get(key, 0) or 0)) + except Exception: + continue + return maximum + + +def _route_lane_summary(report): + max_lane_index = 0 + lane_spacing = 0.0 + lane_max_offset = 0.0 + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + lane = route.get("lane", {}) + if not isinstance(lane, dict): + continue + try: + lane_index = int(lane.get("index", 0) or 0) + except Exception: + lane_index = 0 + if lane_index <= max_lane_index: + continue + max_lane_index = lane_index + try: + lane_spacing = float(lane.get("spacing_mm", 0.0) or 0.0) + except Exception: + lane_spacing = 0.0 + try: + lane_max_offset = float(lane.get("max_offset_mm", 0.0) or 0.0) + except Exception: + lane_max_offset = 0.0 + if max_lane_index <= 0: + return {} + return { + "max_lane_index": max_lane_index, + "spacing_mm": lane_spacing, + "max_offset_mm": lane_max_offset, + } + + +def _route_track_min_capacity(route_track): + if not isinstance(route_track, dict): + return None + capacities = [] + 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 > 0: + capacities.append(capacity) + if not capacities: + return None + return min(capacities) + + +def _route_capacity_pressure_summary(report): + samples = _route_capacity_pressure_samples(report, limit=0) + if not samples: + return {} + pressure = max( + samples, + key=lambda item: int(item.get("max_parallel_wires", 0) or 0), + ) + return { + "count": len(samples), + "max_parallel_wires": pressure.get("max_parallel_wires", 0), + "min_capacity": pressure.get("min_capacity", 0), + "sample": pressure, + } + + +def _route_capacity_pressure_samples(report, limit=8): + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + lane = route.get("lane", {}) + if not isinstance(lane, dict): + continue + try: + max_parallel_wires = int(lane.get("index", 0) or 0) + 1 + except Exception: + max_parallel_wires = 1 + route_capacity = _route_track_min_capacity(route.get("route_track", {})) + if route_capacity is None or max_parallel_wires <= route_capacity: + continue + if max_samples <= 0 or len(samples) < max_samples: + route_track = route.get("route_track", {}) + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "wire": _wire_sample_text(route), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "max_parallel_wires": max_parallel_wires, + "min_capacity": route_capacity, + "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), + } + ) + return samples + + +_ROUTE_QUALITY_WARNING_KIND_LABELS = { + "RoutingRange": "布线面", + "AuxiliaryPath": "辅助路径", +} + + +def _route_track_carrier_kinds(route_track): + if not isinstance(route_track, dict): + return {} + counts = {} + has_segment_list = isinstance(route_track.get("segments"), list) + raw_segments = route_track.get("segments", []) + segments = raw_segments or [] + for segment in segments: + # 虚拟桥接段只表示网络连通,不代表导线真实经过该类型 carrier。 + 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 + kind = str(carrier.get("kind", "") or "").strip() + if kind: + counts[kind] = counts.get(kind, 0) + 1 + if counts: + return counts + if has_segment_list: + return {} + carrier_kinds = route_track.get("carrier_kinds", {}) + if isinstance(carrier_kinds, dict) and carrier_kinds: + return { + str(key): value + for key, value in carrier_kinds.items() + if str(key).strip() + } + return {} + + +def _route_track_carrier_names(route_track, limit=8): + if not isinstance(route_track, dict): + return [] + names = [] + seen = set() + has_segment_list = isinstance(route_track.get("segments"), list) + for segment in route_track.get("segments", []) or []: + # 诊断样例只列真实经过的 carrier;虚拟桥接段不显示为源路径对象。 + 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 + name = str(carrier.get("name", "") or "").strip() + if not name or name in seen: + continue + seen.add(name) + names.append(name) + if len(names) >= int(limit or 0): + return names + if has_segment_list: + return names + return list(route_track.get("carrier_names", []) or [])[: int(limit or 0)] + + +_MAIN_ROUTE_USAGE_KINDS = { + "WireDuct", + "WireDuctOpenEnd", + "UserPath", + "WiringCutOut", +} + + +def _route_path_usage_summary(report): + summary = { + "main_path_routes": 0, + "fallback_routes": 0, + } + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + carrier_kinds = _route_track_carrier_kinds(route.get("route_track", {})) + if any(carrier_kinds.get(kind, 0) for kind in _MAIN_ROUTE_USAGE_KINDS): + summary["main_path_routes"] += 1 + if any(carrier_kinds.get(kind, 0) for kind in _ROUTE_QUALITY_WARNING_KIND_LABELS): + summary["fallback_routes"] += 1 + return summary + + +def _route_network_carrier_kind_counts(network): + counts = {} + if not isinstance(network, dict): + return counts + for carrier in list(network.get("carriers", []) or []): + kind = str(getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or "RoutingPath" + counts[kind] = counts.get(kind, 0) + 1 + return { + key: counts[key] + for key in sorted(counts) + } + + +def _report_route_network_carrier_kind_counts(report): + if not isinstance(report, dict): + return {} + counts = report.get("route_network_carrier_kind_counts", {}) + if not isinstance(counts, dict): + return {} + result = {} + for key, value in counts.items(): + text = str(key or "").strip() + count = _safe_int(value) + if text and count > 0: + result[text] = count + return result + + +_ROUTE_NETWORK_KIND_SUMMARY_LABELS = { + "WireDuct": "线槽路径", + "WireDuctOpenEnd": "线槽开口", + "UserPath": "用户路径", + "WiringCutOut": "过线孔", + "TerminalAccess": "端子接入", + "RoutingRange": "布线面", + "AuxiliaryPath": "辅助路径", +} + + +def _route_network_carrier_kind_summary_text(report): + counts = _report_route_network_carrier_kind_counts(report) + if not counts: + return "" + # 这里展示的是当前可用路径网络,不是本次新生成 carrier 的数量。 + ordered_kinds = ( + "WireDuct", + "WireDuctOpenEnd", + "UserPath", + "WiringCutOut", + "TerminalAccess", + "RoutingRange", + "AuxiliaryPath", + ) + parts = [] + used = set() + for kind in ordered_kinds: + count = _safe_int(counts.get(kind, 0)) + if count <= 0: + continue + used.add(kind) + parts.append("{0} {1} 条".format(_ROUTE_NETWORK_KIND_SUMMARY_LABELS[kind], count)) + for kind, value in sorted(counts.items()): + if kind in used: + continue + count = _safe_int(value) + if count > 0: + parts.append("{0} {1} 条".format(kind, count)) + return ",".join(parts) + + +def _route_network_main_path_carriers(report): + counts = _report_route_network_carrier_kind_counts(report) + return sum(_safe_int(counts.get(kind, 0)) for kind in _MAIN_ROUTE_USAGE_KINDS) + + +def _main_path_not_used(report): + if not isinstance(report, dict): + return False + if _safe_int(report.get("routed", 0)) <= 0: + return False + usage = _route_path_usage_summary(report) + return ( + _safe_int(usage.get("main_path_routes", 0)) <= 0 + and _safe_int(usage.get("fallback_routes", 0)) > 0 + ) + + +def _main_path_not_used_text(report): + if not _main_path_not_used(report): + return "" + usage = _route_path_usage_summary(report) + fallback_routes = _safe_int(usage.get("fallback_routes", 0)) + main_path_carriers = _route_network_main_path_carriers(report) + if main_path_carriers > 0: + return ( + "主路径未采用:当前有线槽/UserPath/过线孔路径 {0} 条,但本批次 {1} 条导线都走了布线面/辅助路径。" + ).format(main_path_carriers, fallback_routes) + return ( + "未使用线槽或用户主路径:本批次 {0} 条导线都走了布线面/辅助路径;" + "请先生成线槽、UserPath 或过线孔主路径。" + ).format(fallback_routes) + + +def _has_routing_path_network_diagnostic(report): + if not isinstance(report, dict): + return False + diagnostic = report.get("routing_path_network_diagnostic", {}) + if not isinstance(diagnostic, dict) or not diagnostic: + return False + return ( + "ok" in diagnostic + or bool(diagnostic.get("issue_codes")) + or bool(diagnostic.get("issues")) + or bool(diagnostic.get("summary")) + ) + + +def _attach_routing_path_network_diagnostic_if_needed(doc, report, opts): + if doc is None or not isinstance(report, dict): + return + # 缺路径网络时也要补诊断。否则用户只能看到 MissingRouteNetwork, + # 看不到是没有线槽/UserPath,还是路径源没有生成 carrier。 + should_attach = _main_path_not_used(report) or _safe_int( + report.get("skipped_missing_route_network", 0) + ) > 0 + if not should_attach: + return + if _has_routing_path_network_diagnostic(report): + return + try: + # 批量布线已经发现“有线但没走主路径”时,补充一次网络诊断,直接给出线槽/端子接入的根因。 + report["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), + ) + ) + except Exception as exc: + report["routing_path_network_diagnostic"] = { + "ok": False, + "issue_count": 1, + "issue_codes": ["routing_path_network_diagnostic_error"], + "issues": [ + { + "severity": "warning", + "code": "routing_path_network_diagnostic_error", + "count": 1, + } + ], + "summary": {}, + "error": str(exc), + } + + +def _route_quality_warning_summary(report): + warning_count = 0 + sample = None + for warning in _route_quality_warning_samples(report, limit=0): + warning_labels = list(warning.get("carrier_labels", []) or []) + if not warning_labels: + continue + warning_count += 1 + if sample is None: + sample = { + "wire": warning.get("wire_label") or warning.get("wire_uuid") or "未知导线", + "labels": warning_labels, + "route_carrier_labels": list(warning.get("route_carrier_labels", []) or []), + } + if warning_count <= 0: + return {} + return { + "count": warning_count, + "sample": sample or {}, + } + + +def _route_quality_warning_samples(report, limit=8): + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + carrier_kinds = _route_track_carrier_kinds(route.get("route_track", {})) + warning_kinds = [ + kind + for kind in _ROUTE_QUALITY_WARNING_KIND_LABELS + if carrier_kinds.get(kind, 0) + ] + if not warning_kinds: + continue + if max_samples <= 0 or len(samples) < max_samples: + route_track = route.get("route_track", {}) + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "carrier_kinds": warning_kinds, + "carrier_labels": [ + _ROUTE_QUALITY_WARNING_KIND_LABELS.get(kind, kind) + for kind in warning_kinds + ], + "route_carrier_labels": _route_warning_carrier_labels( + route_track, + warning_kinds, + ), + } + ) + return samples + + +def _long_network_entry_warning_samples(report, limit=8): + try: + warning_distance = float( + report.get( + "terminal_access_warning_distance", + RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE, + ) + or 0.0 + ) + except Exception: + warning_distance = float(RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE) + if warning_distance <= 0.0: + return [] + + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + long_parts = [] + warning_sides = [] + for side, label, key in ( + ("entry", "起点", "entry_distance"), + ("exit", "终点", "exit_distance"), + ): + try: + distance = float(network.get(key, 0.0) or 0.0) + except Exception: + distance = 0.0 + if distance > warning_distance: + warning_sides.append(side) + long_parts.append("{0}接入 {1:.1f} mm".format(label, distance)) + if not long_parts: + continue + if max_samples <= 0 or len(samples) < max_samples: + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "wire": _wire_sample_text(route), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "entry_distance": float(network.get("entry_distance", 0.0) or 0.0), + "exit_distance": float(network.get("exit_distance", 0.0) or 0.0), + "warning_sides": warning_sides, + "warning_parts": long_parts, + "warning_distance": float(warning_distance), + "route_source_labels": _route_source_labels( + route.get("route_track", {}), + limit=4, + ), + } + ) + return samples + + +def _long_network_entry_summary(report): + samples = _long_network_entry_warning_samples(report, limit=1) + if not samples: + return {} + return { + "count": len(_long_network_entry_warning_samples(report, limit=0)), + "sample": samples[0], + "warning_distance": float(samples[0].get("warning_distance", 0.0) or 0.0), + } + + +def _route_candidate_obstacle_warning_samples(report, limit=8): + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + try: + hits = int(network.get("route_candidate_obstacle_hits", 0) or 0) + except Exception: + hits = 0 + if hits <= 0: + continue + if max_samples <= 0 or len(samples) < max_samples: + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "wire": _wire_sample_text(route), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "hits": hits, + "route_source_labels": _route_source_labels( + route.get("route_track", {}), + limit=4, + ), + } + ) + return samples + + +def _route_candidate_obstacle_warning_summary(report): + samples = _route_candidate_obstacle_warning_samples(report, limit=1) + if not samples: + return {} + return { + "count": len(_route_candidate_obstacle_warning_samples(report, limit=0)), + "sample": samples[0], + } + + +def _route_candidate_boundary_warning_samples(report, limit=8): + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + try: + violations = int(network.get("route_candidate_boundary_violations", 0) or 0) + except Exception: + violations = 0 + if violations <= 0: + continue + if max_samples <= 0 or len(samples) < max_samples: + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "wire": _wire_sample_text(route), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "violations": violations, + "route_source_labels": _route_source_labels( + route.get("route_track", {}), + limit=4, + ), + } + ) + return samples + + +def _route_candidate_boundary_warning_summary(report): + samples = _route_candidate_boundary_warning_samples(report, limit=1) + if not samples: + return {} + return { + "count": len(_route_candidate_boundary_warning_samples(report, limit=0)), + "sample": samples[0], + } + + +def _route_constraint_samples(report, limit=8): + samples = [] + max_samples = int(limit or 0) + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + constraints = network.get("route_constraints", {}) + if not isinstance(constraints, dict) or not constraints: + continue + required = _route_constraint_group(constraints.get("required", {})) + forbidden = _route_constraint_group(constraints.get("forbidden", {})) + if not required and not forbidden: + continue + if max_samples <= 0 or len(samples) < max_samples: + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "wire": _wire_sample_text(route), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "required": required, + "forbidden": forbidden, + } + ) + return samples + + +def _route_constraint_group(group): + if not isinstance(group, dict): + return {} + result = {} + for key in ("names", "labels", "source_names", "source_labels", "kinds"): + values = [] + seen = set() + for item in list(group.get(key, []) or []): + text = str(item or "").strip() + if not text or text in seen: + continue + seen.add(text) + values.append(text) + if values: + result[key] = values + return result + + +def _route_constraint_summary(report): + samples = _route_constraint_samples(report, limit=1) + if not samples: + return {} + return { + "count": len(_route_constraint_samples(report, limit=0)), + "sample": samples[0], + } + + +def _route_constraint_group_text(group): + if not isinstance(group, dict): + return "" + parts = [] + # 中文报告优先显示最容易和 FreeCAD 面板/对象标签对应的标签字段。 + has_source_labels = bool(list(group.get("source_labels", []) or [])) + for key, label in ( + ("labels", ""), + ("names", "名称 "), + ("source_names", "源名称 "), + ("source_labels", "源标签 "), + ("kinds", "类型 "), + ): + if key == "source_names" and has_source_labels: + continue + values = list(group.get(key, []) or []) + if values: + parts.append("{0}{1}".format(label, "、".join(str(item) for item in values))) + return ";".join(parts) + + +_COLLISION_KIND_LABELS = { + "HardIntersection": "硬碰撞", + "ClearanceWarning": "安全间隙", +} + + +_COLLISION_RELATION_LABELS = { + "third_party_device_collision": "第三方设备/布局", + "endpoint_device_collision": "端点设备", + "unbound_obstacle_collision": "未绑定障碍物", + "unknown_collision_relation": "未知关系", +} + + +_STRUCTURAL_COLLISION_KEYWORDS = ( + "cabinet", + "door", + "cover", + "panel", + "bracket", + "support", + "shell", + "柜", + "门", + "盖", + "板", + "支架", + "壳", +) + + +_DEVICE_COLLISION_KEYWORDS = ( + "device", + "mccb", + "breaker", + "terminal", + "relay", + "设备", + "断路器", + "端子", + "继电器", +) + + +def _collision_kind_counts(report): + counts = {} + for sample in _collision_samples_for_report(report): + kind = str(sample.get("collision_kind", "") or "").strip() or "HardIntersection" + counts[kind] = counts.get(kind, 0) + 1 + return counts + + +def _collision_relation_counts(report): + counts = {} + for sample in _collision_samples_for_report(report): + relation = str(sample.get("collision_relation", "") or "").strip() + if not relation: + continue + counts[relation] = counts.get(relation, 0) + 1 + return counts + + +def _collision_samples_for_report(report): + route_samples = [] + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + for sample in list(route.get("collision_samples", []) or []): + if isinstance(sample, dict): + route_samples.append(sample) + if route_samples: + return route_samples + return [ + sample + for sample in list(report.get("collision_samples", []) or []) + if isinstance(sample, dict) + ] + + +def _collision_kind_summary_text(counts): + if not isinstance(counts, dict) or not counts: + return "" + parts = [] + for key in ("HardIntersection", "ClearanceWarning"): + value = int(counts.get(key, 0) or 0) + if value > 0: + parts.append("{0} {1} 处".format(_COLLISION_KIND_LABELS.get(key, key), value)) + for key, value in sorted(counts.items()): + if key in _COLLISION_KIND_LABELS: + continue + value = int(value or 0) + if value > 0: + parts.append("{0} {1} 处".format(key, value)) + return ",".join(parts) + + +def _collision_relation_summary_text(counts): + if not isinstance(counts, dict) or not counts: + return "" + parts = [] + for key in ( + "third_party_device_collision", + "endpoint_device_collision", + "unbound_obstacle_collision", + "unknown_collision_relation", + ): + value = int(counts.get(key, 0) or 0) + if value > 0: + parts.append("{0} {1} 处".format(_COLLISION_RELATION_LABELS.get(key, key), value)) + for key, value in sorted(counts.items()): + if key in _COLLISION_RELATION_LABELS: + continue + value = int(value or 0) + if value > 0: + parts.append("{0} {1} 处".format(key, value)) + return ",".join(parts) + + +def _collision_reroute_recommendation(report): + relation_counts = _collision_relation_counts(report) + third_party_count = _safe_int(relation_counts.get("third_party_device_collision", 0)) + endpoint_count = _safe_int(relation_counts.get("endpoint_device_collision", 0)) + if third_party_count <= 0 and endpoint_count <= 0: + return {} + if third_party_count > 0: + return { + "strategy": "selective_local_reroute_or_user_path", + "global_avoid_obstacles_recommended": False, + "reason": ( + "第三方设备/布局碰撞 {0} 处,优先对碰撞导线做局部二次避障;" + "如果局部绕行仍失败,再补用户路径或调整装配。全量避障会显著放大真实工程计算量。" + ).format(third_party_count), + } + return { + "strategy": "review_terminal_local_access", + "global_avoid_obstacles_recommended": False, + "reason": ( + "端点设备碰撞 {0} 处,优先检查端子局部出线路径、端子朝向和设备模型接线点。" + ).format(endpoint_count), + } + + +def _collision_reroute_recommendation_text(report): + recommendation = _collision_reroute_recommendation(report) + reason = str(recommendation.get("reason", "") or "").strip() + if not reason: + return "" + strategy = str(recommendation.get("strategy", "") or "").strip() + if strategy == "selective_local_reroute_or_user_path": + return "优先对第三方设备/布局碰撞做局部二次避障;局部绕行失败时再补用户路径或调整装配,避免全量避障拖慢真实工程。" + if strategy == "review_terminal_local_access": + return "优先检查端子局部出线路径、端子朝向和设备模型接线点。" + return reason + + +def _selective_collision_reroute_summary_text(report): + attempts = _safe_int(report.get("selective_collision_reroute_attempts", 0)) + accepted = _safe_int(report.get("selective_collision_reroutes", 0)) + rejected_fallback = _safe_int(report.get("selective_collision_reroute_rejected_fallback", 0)) + no_improvement = _safe_int(report.get("selective_collision_reroute_no_improvement", 0)) + errors = _safe_int(report.get("selective_collision_reroute_errors", 0)) + if attempts <= 0: + return "" + parts = [ + "尝试 {0} 条".format(attempts), + "接受 {0} 条".format(accepted), + ] + if rejected_fallback > 0: + parts.append("拒绝辅助路径 {0} 条".format(rejected_fallback)) + if no_improvement > 0: + parts.append("无改善 {0} 条".format(no_improvement)) + if errors > 0: + parts.append("失败 {0} 条".format(errors)) + text = "局部避障:{0}".format(",".join(parts)) + if rejected_fallback > 0: + text += ";拒绝的绕行会退回布线面/辅助路径,请补主路径/UserPath 或调整装配" + return text + + +def _top_collision_obstacle_summary_text(report, limit=3): + obstacles = _top_collision_obstacles(report, limit=limit) + if not obstacles: + return "" + return _top_collision_obstacle_items_text(obstacles) + + +def _main_path_detour_bridge_pair_text(detour_summary, limit=3): + if not isinstance(detour_summary, dict): + return "" + pair_counts = detour_summary.get("bridge_pair_counts", {}) + if not isinstance(pair_counts, dict) or not pair_counts: + return "" + items = [] + for pair, count in sorted(pair_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0])))[: int(limit or 3)]: + pair_text = str(pair or "").strip() + count_value = _safe_int(count) + if pair_text and count_value > 0: + items.append("{0} {1} 条".format(pair_text, count_value)) + return "、".join(items) + + +def _top_collision_resolution_summary_text(report, limit=3): + obstacles = _top_collision_obstacles(report, limit=limit) + parts = [] + for item in obstacles: + label = str(item.get("label", "") or "").strip() + hint = str(item.get("resolution_hint_label", "") or "").strip() + if label and hint: + parts.append("{0}:{1}".format(label, hint)) + return ";".join(parts) + + +def _collision_resolution_summary(report, limit=8): + obstacles = _top_collision_obstacles(report, limit=limit) + counts = {} + samples = {} + for item in obstacles: + code = str(item.get("resolution_hint_code", "") or "").strip() + if not code: + continue + counts[code] = counts.get(code, 0) + 1 + samples.setdefault(code, []) + if len(samples[code]) < 5: + samples[code].append( + { + "label": item.get("label", ""), + "name": item.get("name", ""), + "count": int(item.get("count", 0) or 0), + "parent_labels": list(item.get("parent_labels", []) or []), + "parent_names": list(item.get("parent_names", []) or []), + "resolution_hint_label": item.get("resolution_hint_label", ""), + } + ) + if not counts: + return {} + + structural_count = int(counts.get("review_pass_through_structural_obstacle", 0) or 0) + device_count = int(counts.get("review_device_or_layout_collision", 0) or 0) + action_parts = [] + if structural_count > 0: + action_parts.append( + "先处理 {0} 个疑似结构件碰撞候选:确认后可标记 PassThrough".format(structural_count) + ) + if device_count > 0: + action_parts.append( + "另有 {0} 个疑似设备/装配碰撞需要补路径或调整装配".format(device_count) + ) + recommended_action = ";".join(action_parts) + if recommended_action: + recommended_action += "。" + return { + "counts": counts, + "samples": samples, + "recommended_action": recommended_action, + } + + +def _collision_resolution_counts(report): + summary = _collision_resolution_summary(report, limit=999999) + counts = summary.get("counts", {}) if isinstance(summary, dict) else {} + return counts if isinstance(counts, dict) else {} + + +def _top_collision_obstacle_items_text(obstacles): + parts = [] + for item in obstacles: + label = str(item.get("label", "") or "").strip() + parent_labels = [ + str(parent or "").strip() + for parent in list(item.get("parent_labels", []) or []) + if str(parent or "").strip() + ] + display = label + if parent_labels: + display = "{0}({1})".format(label, parent_labels[0]) + parts.append("{0} {1} 处".format(display, item.get("count", 0))) + return ",".join(parts) + + +def _collision_obstacle_resolution_hint(item): + if not isinstance(item, dict): + return { + "code": "review_device_or_layout_collision", + "label": "疑似设备/安装区域碰撞,优先补柜内路径或调整装配", + } + text_parts = [ + item.get("label", ""), + item.get("name", ""), + ] + own_text = " ".join(str(part or "").lower() for part in text_parts) + if any(keyword in own_text for keyword in _DEVICE_COLLISION_KEYWORDS): + return { + "code": "review_device_or_layout_collision", + "label": "疑似设备/安装区域碰撞,优先补柜内路径或调整装配", + } + text_parts.extend(list(item.get("parent_labels", []) or [])) + text_parts.extend(list(item.get("parent_names", []) or [])) + text = " ".join(str(part or "").lower() for part in text_parts) + if any(keyword in text for keyword in _STRUCTURAL_COLLISION_KEYWORDS): + return { + "code": "review_pass_through_structural_obstacle", + "label": "疑似柜体/门板/支架结构,确认可穿越后标记忽略碰撞", + } + return { + "code": "review_device_or_layout_collision", + "label": "疑似设备/安装区域碰撞,优先补柜内路径或调整装配", + } + + +def _top_collision_obstacles(report, limit=3): + groups = {} + for sample in _collision_samples_for_report(report): + label = ( + str(sample.get("obstacle_label", "") or "").strip() + or str(sample.get("obstacle_name", "") or "").strip() + ) + if not label: + continue + name = str(sample.get("obstacle_name", "") or "").strip() + key = (label, name) + group = groups.setdefault( + key, + { + "label": label, + "name": name, + "count": 0, + "collision_kind_counts": {}, + "collision_relation_counts": {}, + "parent_labels": [], + "parent_names": [], + }, + ) + group["count"] += 1 + element_uuid = str(sample.get("obstacle_element_uuid", "") or "").strip() + if element_uuid and not group.get("element_uuid"): + group["element_uuid"] = element_uuid + instance_id = str(sample.get("obstacle_instance_id", "") or "").strip() + if instance_id and not group.get("instance_id"): + group["instance_id"] = instance_id + kind = str(sample.get("collision_kind", "") or "").strip() or "HardIntersection" + group["collision_kind_counts"][kind] = group["collision_kind_counts"].get(kind, 0) + 1 + relation = str(sample.get("collision_relation", "") or "").strip() + if relation: + group["collision_relation_counts"][relation] = ( + group["collision_relation_counts"].get(relation, 0) + 1 + ) + for parent_label in list(sample.get("obstacle_parent_labels", []) or []): + parent_label = str(parent_label or "").strip() + if parent_label and parent_label not in group["parent_labels"]: + group["parent_labels"].append(parent_label) + for parent_name in list(sample.get("obstacle_parent_names", []) or []): + parent_name = str(parent_name or "").strip() + if parent_name and parent_name not in group["parent_names"]: + group["parent_names"].append(parent_name) + if not groups: + return [] + items = sorted( + groups.values(), + key=lambda item: (-int(item.get("count", 0) or 0), item.get("label", ""), item.get("name", "")), + )[: int(limit or 3)] + for item in items: + if not item.get("collision_relation_counts"): + item.pop("collision_relation_counts", None) + hint = _collision_obstacle_resolution_hint(item) + item["resolution_hint_code"] = hint["code"] + item["resolution_hint_label"] = hint["label"] + return items + + +def _wire_style_status_counts(report): + counts = {} + if isinstance(report.get("wire_style_status_counts"), dict): + for key, value in (report.get("wire_style_status_counts") or {}).items(): + status = str(key or "").strip() + if not status: + continue + try: + amount = int(value or 0) + except Exception: + amount = 0 + if amount > 0: + counts[status] = counts.get(status, 0) + amount + if counts: + return counts + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + status = str(route.get("wire_style_status", "") or "").strip() + if status: + counts[status] = counts.get(status, 0) + 1 + return counts + + +def _wire_style_status_summary_text(counts): + if not isinstance(counts, dict) or not counts: + return "" + labels = { + "Missing": "缺失", + "Resolved": "已解析", + } + parts = [] + # 缺失样式最影响手动测试判断,所以中文报告里优先显示。 + for key in ("Missing", "Resolved"): + value = int(counts.get(key, 0) or 0) + if value > 0: + parts.append("{0} {1} 条".format(labels.get(key, key), value)) + for key, value in sorted(counts.items()): + if key in labels: + continue + value = int(value or 0) + if value > 0: + parts.append("{0} {1} 条".format(key, value)) + return ",".join(parts) + + +def _wire_style_status_samples(report, status="Missing", limit=8): + wanted = str(status or "").strip() + samples = [] + max_samples = int(limit or 0) + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + if str(route.get("wire_style_status", "") or "").strip() != wanted: + continue + if max_samples > 0 and len(samples) >= max_samples: + break + samples.append( + { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire": _wire_sample_text(route), + "wire_style_id": route.get("wire_style_id", ""), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "start_terminal_display": route.get("start_terminal_display", ""), + "end_terminal_display": route.get("end_terminal_display", ""), + } + ) + return samples + + +def _has_routing_attempt_without_results(report): + if not isinstance(report, dict): + return False + if _safe_int(report.get("total_wires", 0)) <= 0: + return False + if _safe_int(report.get("routed", 0)) > 0: + return False + status_counts = report.get("route_status_counts", {}) + has_status = isinstance(status_counts, dict) and any( + _safe_int(value) > 0 for value in status_counts.values() + ) + return bool( + has_status + or _safe_int(report.get("skipped_missing_terminal", 0)) > 0 + or _safe_int(report.get("skipped_missing_route_network", 0)) > 0 + or _safe_int(report.get("skipped_invalid", 0)) > 0 + or bool(report.get("errors")) + or bool(report.get("error_samples")) + ) + + +def _route_status_count(report, status): + if not isinstance(report, dict): + return 0 + counts = report.get("route_status_counts", {}) + if not isinstance(counts, dict): + return 0 + return _safe_int(counts.get(status, 0)) + + +def _route_status_counts_payload(report): + if not isinstance(report, dict): + return {} + counts = report.get("route_status_counts", {}) + if not isinstance(counts, dict): + return {} + payload = {} + for key, value in counts.items(): + text = str(key or "").strip() + count = _safe_int(value) + if text and count > 0: + payload[text] = count + return payload + + +def _route_status_counts_text(counts): + if not isinstance(counts, dict) or not counts: + return "" + labels = { + "Routed": "正常", + "CollisionWarning": "碰撞告警", + "Error": "错误", + "MissingTerminal": "缺失端子", + "MissingRouteNetwork": "缺少布线路径网络", + "Invalid": "无效任务", + } + parts = [] + for key in ("Routed", "CollisionWarning", "Error", "MissingTerminal", "MissingRouteNetwork", "Invalid"): + value = _safe_int(counts.get(key, 0)) + if value > 0: + parts.append("{0} {1} 条".format(labels[key], value)) + for key in sorted(counts): + if key in labels: + continue + value = _safe_int(counts.get(key, 0)) + if value > 0: + parts.append("{0} {1} 条".format(key, value)) + return ",".join(parts) + + +def _has_routing_error_status(report): + if not isinstance(report, dict): + return False + return ( + bool(report.get("errors")) + or bool(report.get("error_samples")) + or _route_status_count(report, "Error") > 0 + ) + + +def format_eplan_connection_route_report(report): + message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( + report.get("routed", 0), + report.get("collision_warnings", 0), + report.get("skipped_missing_terminal", 0), + ) + if _safe_int(report.get("total_wires", 0)) <= 0: + message += "\n没有导线任务:请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中已有导线任务。" + status_counts = report.get("route_status_counts", {}) + if isinstance(status_counts, dict) and status_counts: + status_labels = { + "Routed": "正常", + "CollisionWarning": "碰撞告警", + "Error": "错误", + "MissingTerminal": "缺失端子", + "MissingRouteNetwork": "缺少布线路径网络", + "Invalid": "无效任务", + } + def status_count_value(value): + try: + return int(value or 0) + except Exception: + return 0 + status_parts = [] + for key in ( + "Routed", + "CollisionWarning", + "Error", + "MissingTerminal", + "MissingRouteNetwork", + "Invalid", + ): + value = status_count_value(status_counts.get(key, 0)) + if value > 0: + status_parts.append("{0} {1} 条".format(status_labels[key], value)) + for key, value in sorted(status_counts.items()): + value = status_count_value(value) + if key in status_labels or value <= 0: + continue + status_parts.append("{0} {1} 条".format(key, value)) + if status_parts: + message += "\n结果状态:{0}。".format(",".join(status_parts)) + if _has_routing_attempt_without_results(report): + message += "\n未生成有效导线:本次只有路径承载/诊断对象,未生成 RoutedConnection 导线。" + style_status_text = _wire_style_status_summary_text(_wire_style_status_counts(report)) + if style_status_text: + message += "\n导线样式:{0}。".format(style_status_text) + missing_style_samples = _wire_style_status_samples(report, status="Missing", limit=1) + if missing_style_samples: + sample = missing_style_samples[0] + message += " 示例导线 {0} 样式 {1}。".format( + sample.get("wire", "未知导线"), + sample.get("wire_style_id", ""), + ) + style_database_path = str(report.get("wire_style_database_path", "") or "").strip() + if style_database_path: + fallback_from = str(report.get("wire_style_database_fallback_from", "") or "").strip() + if fallback_from: + message += "\n导线样式库:{0}(从备用库恢复,原库:{1})。".format( + style_database_path, + fallback_from, + ) + else: + message += "\n导线样式库:{0}。".format(style_database_path) + prepared_layout = report.get("prepared_layout") + if isinstance(prepared_layout, dict): + message += "\n布线布局空间:线槽路径 {0} 条,布线面 {1} 条,端子接入 {2} 条。".format( + prepared_layout.get("wire_duct_carriers", 0), + prepared_layout.get("surface_carriers", 0), + prepared_layout.get("terminal_access_carriers", 0), + ) + route_network_text = _route_network_carrier_kind_summary_text(report) + if route_network_text: + message += "\n当前路径网络:{0}。".format(route_network_text) + auto_bridges = report.get("auto_diagnostic_bridges", {}) + if isinstance(auto_bridges, dict): + created_count = _safe_int(auto_bridges.get("created_count", 0)) + if created_count > 0: + message += "\n自动诊断桥接:生成 UserPath {0} 条。".format(created_count) + auto_detour_bridges = report.get("auto_main_path_detour_bridges", {}) + if isinstance(auto_detour_bridges, dict): + created_count = _safe_int(auto_detour_bridges.get("created_count", 0)) + 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) + 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 = [ + _routing_path_network_issue_label(code) + for code in list(path_diagnostic.get("issue_codes", []) or [])[:3] + ] + message += "\n路径网络检查提示:{0}。".format("、".join(issue_labels) if issue_labels else "存在问题") + outside_sources = _dict_items(path_diagnostic.get("route_carriers_outside_boundary", []) or []) + if outside_sources: + sample = outside_sources[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + carrier_text = carrier.get("label") or carrier.get("name") or "未知路径对象" + message += " 越界路径:{0} {1} 个越界点。".format( + carrier_text, + _safe_int(sample.get("outside_point_count", 0)), + ) + outside_terminals = _dict_items(path_diagnostic.get("terminals_outside_boundary", []) or []) + if outside_terminals: + sample = outside_terminals[0] + message += " 越界端子:{0} {1} 个越界点。".format( + _diagnostic_terminal_text(sample), + _safe_int(sample.get("outside_point_count", 0)), + ) + if report.get("skipped_missing_route_network", 0) > 0: + message += "\n缺少或未连通布线路径网络:{0} 条导线已跳过。请检查线槽/UserPath/布线面是否已生成 carrier、两端是否接入同一网络,以及路径约束是否过严。".format( + report.get("skipped_missing_route_network", 0) + ) + routing_sources = report.get("routing_sources", {}) + if isinstance(routing_sources, dict) and routing_sources: + candidate_sources = _safe_int(routing_sources.get("candidate_sources", 0)) + route_carriers = _safe_int(routing_sources.get("route_carriers", 0)) + if candidate_sources <= 0 and route_carriers <= 0: + message += " 未识别到线槽、布线面或用户路径源。" + elif route_carriers <= 0: + message += " 已识别到布线源 {0} 个,但还没有生成可用路径 carrier。".format( + candidate_sources + ) + route_sample = (report.get("missing_route_network_samples") or [None])[0] + if route_sample: + message += "\n缺路径网络示例:导线 {0},{1}。".format( + _wire_sample_text(route_sample), + _endpoint_pair_text(route_sample), + ) + # 示例带上原始失败原因,手动测试时可以直接判断是空网络、端点不连通还是距离阈值限制。 + route_error = str(route_sample.get("error", "") or "").strip() + if route_error: + message += "原因:{0}。".format(route_error) + retry_count = _safe_int(report.get("missing_route_retries", 0)) + if retry_count > 0: + retry_limit = _safe_int(report.get("missing_route_retry_candidate_limit", 0)) + if retry_limit > 0: + message += "\n候选放宽重试:{0} 条导线通过候选上限 {1} 的补救重试完成布线。".format( + retry_count, + retry_limit, + ) + else: + message += "\n候选放宽重试:{0} 条导线通过补救重试完成布线。".format( + retry_count + ) + total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) + if total_length_mm > 0.0: + message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) + replaced_routed_connections = int(report.get("replaced_routed_connections", 0) or 0) + if replaced_routed_connections > 0: + message += "\n已替换旧布线连接:{0} 条。".format(replaced_routed_connections) + hidden_route_carriers = int(report.get("hidden_route_carriers", 0) or 0) + if hidden_route_carriers > 0: + message += "\n已隐藏走线路径辅助对象:{0} 条。".format(hidden_route_carriers) + bridged_segments = _route_network_metric_max(report, "bridged_segments") + blocked_segments = _route_network_metric_max(report, "blocked_segments") + network_parts = [] + if bridged_segments > 0: + network_parts.append("自动桥接 {0} 段相邻/投影主路径".format(bridged_segments)) + if blocked_segments > 0: + network_parts.append("避障屏蔽 {0} 段".format(blocked_segments)) + if network_parts: + message += "\n路径网络:{0}。".format(",".join(network_parts)) + lane_summary = _route_lane_summary(report) + if lane_summary: + lane_text = "并行错位:最大 lane {0},间距 {1:.1f} mm".format( + lane_summary.get("max_lane_index", 0), + float(lane_summary.get("spacing_mm", 0.0) or 0.0), + ) + max_offset = float(lane_summary.get("max_offset_mm", 0.0) or 0.0) + if max_offset > 0.0: + lane_text += ",最大偏移 {0:.1f} mm".format(max_offset) + message += "\n{0}。".format(lane_text) + capacity_pressure = _route_capacity_pressure_summary(report) + if capacity_pressure: + message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}。".format( + capacity_pressure.get("max_parallel_wires", 0), + capacity_pressure.get("min_capacity", 0), + ) + sample = capacity_pressure.get("sample", {}) + if isinstance(sample, dict) and sample.get("wire"): + message += " 示例导线 {0}".format(sample.get("wire")) + route_labels = list(sample.get("route_source_labels", []) or []) + carrier_names = list(sample.get("carrier_names", []) or []) + path_labels = route_labels or carrier_names + if path_labels: + message += ",路径 {0}".format("、".join(path_labels)) + message += "。" + collision_kind_text = _collision_kind_summary_text(_collision_kind_counts(report)) + if collision_kind_text: + message += "\n碰撞分类:{0}。".format(collision_kind_text) + collision_relation_text = _collision_relation_summary_text(_collision_relation_counts(report)) + if collision_relation_text: + message += "\n碰撞关系:{0}。".format(collision_relation_text) + reroute_text = _collision_reroute_recommendation_text(report) + if reroute_text: + message += "\n后续处理:{0}".format(reroute_text) + selective_reroute_text = _selective_collision_reroute_summary_text(report) + if selective_reroute_text: + message += "\n{0}。".format(selective_reroute_text) + detour_summary = report.get("main_path_detour_missing_summary", {}) + if isinstance(detour_summary, dict): + detour_count = _safe_int(detour_summary.get("wire_count", 0)) + if detour_count > 0: + message += "\n缺主路径绕行:{0} 条".format(detour_count) + label_counts = detour_summary.get("rejected_fallback_label_counts", {}) + location_items = [] + if isinstance(label_counts, dict) and label_counts: + for label, count in sorted(label_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0])))[:5]: + label_text = str(label or "").strip() + count_value = _safe_int(count) + if label_text and count_value > 0: + location_items.append("{0} {1} 条".format(label_text, count_value)) + if location_items: + message += ",需补路径位置:{0}".format("、".join(location_items)) + bridge_pair_text = _main_path_detour_bridge_pair_text(detour_summary) + if bridge_pair_text: + message += ";补路配对:{0}".format(bridge_pair_text) + message += "。" + top_collision_obstacles = _top_collision_obstacle_summary_text(report) + if top_collision_obstacles: + message += "\n碰撞高发对象:{0}。".format(top_collision_obstacles) + collision_resolution = _top_collision_resolution_summary_text(report) + if collision_resolution: + message += "\n碰撞处理建议:{0}。".format(collision_resolution) + candidate_ranks = [] + for route in report.get("routes", []) or []: + if not isinstance(route, dict): + continue + network = route.get("network", {}) + if not isinstance(network, dict): + continue + try: + entry_rank = int(network.get("entry_candidate_rank", 0) or 0) + except Exception: + entry_rank = 0 + try: + exit_rank = int(network.get("exit_candidate_rank", 0) or 0) + except Exception: + exit_rank = 0 + if entry_rank > 1 or exit_rank > 1: + candidate_ranks.append((entry_rank, exit_rank)) + if candidate_ranks: + entry_rank, exit_rank = candidate_ranks[0] + parts = [] + if entry_rank > 1: + parts.append("起点第 {0} 个".format(entry_rank)) + if exit_rank > 1: + parts.append("终点第 {0} 个".format(exit_rank)) + message += "\n接入候选:{0}。".format(",".join(parts)) + long_entry_warning = _long_network_entry_summary(report) + if long_entry_warning: + sample = long_entry_warning.get("sample", {}) + route_text = "" + 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( + long_entry_warning.get("count", 0), + sample.get("wire", "未知导线"), + ",".join(sample.get("warning_parts", []) or []), + route_text, + ) + obstacle_entry_warning = _route_candidate_obstacle_warning_summary(report) + if obstacle_entry_warning: + sample = obstacle_entry_warning.get("sample", {}) + route_text = "" + 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}。请补 UserPath/线槽或移动设备。".format( + obstacle_entry_warning.get("count", 0), + sample.get("wire", "未知导线"), + sample.get("hits", 0), + route_text, + ) + boundary_warning = _route_candidate_boundary_warning_summary(report) + if boundary_warning: + sample = boundary_warning.get("sample", {}) + route_text = "" + 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}。请补柜内 UserPath/线槽或调整柜内边界。".format( + boundary_warning.get("count", 0), + sample.get("wire", "未知导线"), + sample.get("violations", 0), + route_text, + ) + constraint_summary = _route_constraint_summary(report) + if constraint_summary: + sample = constraint_summary.get("sample", {}) + constraint_parts = [] + required_text = _route_constraint_group_text(sample.get("required", {})) + forbidden_text = _route_constraint_group_text(sample.get("forbidden", {})) + if required_text: + constraint_parts.append("必须经过 {0}".format(required_text)) + if forbidden_text: + constraint_parts.append("禁止经过 {0}".format(forbidden_text)) + sample_text = ",".join(constraint_parts) if constraint_parts else "存在路径约束" + message += "\n路径约束提示:{0} 条导线应用必经/禁经规则,示例导线 {1} {2}。".format( + constraint_summary.get("count", 0), + sample.get("wire", "未知导线"), + sample_text, + ) + route_source_sample = _route_source_sample_text(report) + if route_source_sample: + message += "\n{0}".format(route_source_sample) + path_usage = _route_path_usage_summary(report) + main_path_routes = _safe_int(path_usage.get("main_path_routes", 0)) + fallback_routes = _safe_int(path_usage.get("fallback_routes", 0)) + if main_path_routes > 0 or fallback_routes > 0: + message += "\n路径采用:线槽/主路径 {0} 条,布线面/辅助路径 {1} 条。".format( + main_path_routes, + fallback_routes, + ) + main_path_text = _main_path_not_used_text(report) + if main_path_text: + message += "\n{0}".format(main_path_text) + quality_warning = _route_quality_warning_summary(report) + if quality_warning: + message += "\n路径质量提示:{0} 条导线使用布线面/辅助路径,可能没有完全优先进入线槽。".format( + quality_warning.get("count", 0) + ) + sample = quality_warning.get("sample", {}) + if isinstance(sample, dict) and sample.get("wire") and sample.get("labels"): + message += " 示例 {0} 使用{1}".format( + sample.get("wire"), + "、".join(sample.get("labels", []) or []), + ) + route_carrier_labels = list(sample.get("route_carrier_labels", []) or []) + if route_carrier_labels: + message += ":{0}".format("、".join(route_carrier_labels)) + message += "。" + errors = report.get("errors", []) or [] + if errors: + message += "\n首个错误:{0}".format(str(errors[0])) + error_sample = (report.get("error_samples") or [None])[0] + if error_sample: + message += "\n错误示例:导线 {0},{1}:{2}".format( + _wire_sample_text(error_sample), + _endpoint_pair_text(error_sample), + error_sample.get("error", ""), + ) + collision_sample = (report.get("collision_samples") or [None])[0] + if collision_sample: + obstacle_text = ( + collision_sample.get("obstacle_label") + or collision_sample.get("obstacle_name") + or "未知对象" + ) + wire_text = _wire_object_sample_text(collision_sample) + route_text = "" + route_labels = list(collision_sample.get("route_source_labels", []) or []) + if route_labels: + route_text = ",路径 {0}".format("、".join(route_labels)) + if collision_sample.get("collision_kind") == "ClearanceWarning": + message += "\n碰撞示例:导线 {0} 进入 {1} 的安全间隙{2}。".format( + wire_text, + obstacle_text, + route_text, + ) + else: + message += "\n碰撞示例:导线 {0} 碰到 {1}{2}。".format( + wire_text, + obstacle_text, + route_text, + ) + auto_bound = report.get("auto_bound_terminals", 0) + auto_created = report.get("auto_created_terminals", 0) + if auto_bound or auto_created: + message += "\n已按导线任务绑定 3D 工程端子:更新 {0} 个,新建 {1} 个。".format( + auto_bound, + auto_created, + ) + missing_reason_counts = {} + if report.get("skipped_missing_terminal", 0) > 0: + missing_reason_counts = _missing_endpoint_reason_counts_from_samples(report.get("missing_endpoint_samples", [])) + if report.get("routed", 0) == 0 and report.get("skipped_missing_terminal", 0) > 0: + message += ( + "\n端子匹配失败:当前 3D 可布线端子 {0} 个,其中本地模板端子 {1} 个;" + "导线任务引用的 QET terminal_uuid 没有绑定到这些 3D 工程端子。" + ).format( + report.get("available_terminals", 0), + report.get("local_terminals", 0), + ) + if report.get("local_terminals", 0) > 0: + message += " 请先从 QET 重新导入/更新工程端子,使端子 UUID 不再是 local:...。" + missing_reason_hint = _missing_endpoint_reason_hint_text(missing_reason_counts) + if missing_reason_hint: + message += "\n缺端子原因提示:{0}".format(missing_reason_hint) + missing_terminal_summary = _batch_missing_terminal_summary(report) + missing_device_groups_text = _missing_terminal_device_groups_text( + missing_terminal_summary.get("device_groups", []) + ) + if missing_device_groups_text: + message += "\n需补端子设备:{0}。".format(missing_device_groups_text) + sample = (report.get("missing_endpoint_samples") or [None])[0] + if sample: + endpoint_text = "{0} -> {1}{2}".format( + _missing_endpoint_label(sample, "start"), + _missing_endpoint_label(sample, "end"), + _missing_endpoint_side_summary(sample), + ) + wire_text = _wire_object_sample_text(sample) + if wire_text and wire_text != "未知导线": + message += "\n缺失示例:导线 {0},{1}".format(wire_text, endpoint_text) + else: + message += "\n缺失示例:{0}".format(endpoint_text) + detail_text = _missing_endpoint_detail_text(sample) + if detail_text: + message += "\n缺失明细:{0}。".format(detail_text) + return message + + +def _clear_routing_preflight_diagnostics(doc): + group = WiringObjects.ensure_diagnostic_group(doc, _project_uuid(doc)) + removed = 0 + for obj in list(getattr(group, "Group", []) or []): + if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "RoutingPreflight": + continue + try: + group.removeObject(obj) + except Exception: + try: + group.Group = [ + candidate + for candidate in list(getattr(group, "Group", []) or []) + if candidate is not obj + ] + except Exception: + pass + try: + if doc.getObject(getattr(obj, "Name", "")) is not None: + doc.removeObject(obj.Name) + removed += 1 + except Exception: + pass + return removed + + +def _compact_routing_preflight_report(report, sample_limit=8): + if not isinstance(report, dict): return {} + limit = max(int(sample_limit or 0), 0) + payload = {} + for key in ( + "ok", + "source", + "runtime_version", + "project_uuid", + "total_wires", + "available_terminals", + "local_terminals", + "route_network_carriers", + "route_network_segments", + "route_network_nodes", + "route_network_error", + "routeability_checked", + "routeability_sample_limit", + "routeability_eligible_wires", + "routeability_unchecked_wires", + "unrouteable_wires", + ): + if key in report: + payload[key] = report.get(key) + issue_codes = list(report.get("issue_codes", []) or []) + payload["issue_codes"] = issue_codes[:50] + issues = [item for item in list(report.get("issues", []) or []) if isinstance(item, dict)] + payload["issues"] = issues[:limit] + payload["issue_count"] = len(issues) + missing_endpoint_uuids = list(report.get("missing_endpoint_uuids", []) or []) + payload["missing_endpoint_uuid_count"] = len(missing_endpoint_uuids) + payload["missing_endpoint_uuids"] = missing_endpoint_uuids[:50] + missing_samples = list(report.get("missing_endpoint_samples", []) or []) + payload["missing_endpoint_samples"] = missing_samples[:limit] + payload["missing_endpoint_samples_count"] = len(missing_samples) + unrouteable_samples = list(report.get("unrouteable_samples", []) or []) + payload["unrouteable_samples"] = unrouteable_samples[:limit] + payload["unrouteable_samples_count"] = len(unrouteable_samples) + for key in ( + "routing_sources", + "routing_boundaries", + "routing_obstacle_modes", + "routing_path_network_diagnostic", + "runtime_capabilities", + "wire_style_database", + "wire_style", + ): + value = report.get(key) + if isinstance(value, dict): + payload[key] = dict(value) + payload["diagnostic_payload"] = "compact-routing-preflight-v1" + return payload + + +def _diagnostic_issue_codes_text(issue_codes): + values = [] + seen = set() + for code in list(issue_codes or []): + text = str(code or "").strip() + if not text or text in seen: + continue + seen.add(text) + values.append(text) + return ", ".join(values) + + +def _diagnostic_issue_labels_text(issue_codes): + values = [] + seen = set() + for code in list(issue_codes or []): + label = _routing_diagnostic_issue_label(code) + if not label or label in seen: + continue + seen.add(label) + values.append(label) + return "、".join(values) + + +def write_routing_preflight_diagnostic(doc, report): + if doc is None or not isinstance(report, dict): + return None + project_uuid = str(report.get("project_uuid", "") or _project_uuid(doc)).strip() + group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) + _clear_routing_preflight_diagnostics(doc) + compact_payload = _compact_routing_preflight_report(report) + # 预检报告会被用户反复刷新,只保留压缩后的最新一次结果,便于在树中排障。 + diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETRoutingPreflightDiagnostic")) + diagnostic.Label = "QET Routing Preflight Diagnostic" + _set_string(diagnostic, "QetDiagnosticKind", "RoutingPreflight", "QET diagnostic kind") + _set_string(diagnostic, "QetProjectUuid", project_uuid, "Project UUID") + _set_bool(diagnostic, "QetDiagnosticOk", bool(report.get("ok", False)), "QET diagnostic pass state") + _set_string( + diagnostic, + "QetDiagnosticIssueCodes", + _diagnostic_issue_codes_text(compact_payload.get("issue_codes", [])), + "QET routing diagnostic issue codes", + ) + _set_string( + diagnostic, + "QetDiagnosticIssueLabels", + _diagnostic_issue_labels_text(compact_payload.get("issue_codes", [])), + "QET routing diagnostic issue labels", + ) + _set_string( + diagnostic, + "QetDiagnosticMessage", + format_eplan_routing_preflight_report(report), + "QET routing preflight diagnostic message", + ) + _set_string( + diagnostic, + "QetDiagnosticJson", + json.dumps(compact_payload, ensure_ascii=False), + "QET routing preflight diagnostic payload", + ) + group.addObject(diagnostic) + return diagnostic + + +_ROUTING_DIAGNOSTIC_KINDS = ( + "RoutingPreflight", + "RoutingPathNetwork", + "RoutingConnectionBatch", +) + + +_ROUTING_DIAGNOSTIC_ISSUE_LABELS = { + "no_wire_tasks": "没有导线任务", + "no_available_terminals": "没有工程端子", + "missing_endpoints": "缺失端点", + "unrouteable_wires": "导线不可达", + "no_routed_connections": "未生成有效导线", + "missing_terminals": "端子匹配失败", + "no_route_network": "缺少路径网络", + "missing_route_network": "缺少路径网络", + "no_routing_sources": "缺少布线源", + "routing_sources_not_generated": "布线源未生成路径网络", + "wire_style_database_not_configured": "导线样式库未配置", + "wire_style_database_missing": "导线样式库文件不存在", + "wire_style_database_no_table": "导线样式库缺少 wire_properties", + "wire_style_database_empty": "导线样式库为空", + "wire_style_database_unreadable": "导线样式库无法读取", + "runtime_route_constraint_collector_missing": "运行模块缺少路径约束收集函数", + "missing_wire_styles": "缺失导线样式", + "wires_without_style_id": "导线未设置样式", + "empty_routing_path_network": "布线路径网络为空", + "invalid_route_carriers": "路径对象几何无效", + "routing_range_only_network": "仅使用布线面兜底", + "main_path_not_used": "未使用线槽或用户主路径", + "invalid_terminal_local_routes": "端子局部路径无效", + "route_carriers_outside_boundary": "路径越出柜内边界", + "terminals_outside_boundary": "端子越出柜内边界", + "long_terminal_accesses": "端子接入过长", + "long_terminal_access": "端子接入过长", + "unconnected_terminals": "端子未接入", + "wire_duct_endpoint_breaks": "线槽端点疑似断开", + "isolated_network_components": "存在孤立路径网络", + "routing_errors": "布线计算错误", + "collision_warnings": "碰撞告警", + "structural_collision_candidates": "结构件碰撞候选", + "device_or_layout_collisions": "设备/布局碰撞", + "third_party_device_collisions": "第三方设备/布局碰撞", + "endpoint_device_collisions": "端点设备碰撞", + "main_path_detour_missing": "缺少主路径绕行空间", + "route_quality_warnings": "路径质量告警", + "route_candidate_obstacle_hits": "候选路径碰撞风险", + "route_candidate_boundary_violations": "候选路径越出柜内边界", + "route_capacity_pressure": "路径容量压力", + "diagnostic_json_empty": "诊断 JSON 为空", + "diagnostic_json_invalid": "诊断 JSON 无效", + "routed_wire_diagnostics_missing": "导线诊断缺失", + "routed_wire_diagnostics_invalid": "导线诊断 JSON 无效", + "missing_device_binding_metadata": "端点缺少绑定信息", + "device_not_in_3d_scene": "3D场景缺少设备", + "no_3d_terminals_for_element": "设备缺少工程端子", + "no_3d_terminals_for_instance": "实例缺少工程端子", + "terminal_uuid_not_in_element": "端子UUID不匹配", +} + + +def _routing_diagnostic_issue_label(code): + text = str(code or "").strip() + return _ROUTING_DIAGNOSTIC_ISSUE_LABELS.get(text, text or "未知问题") + + +def _read_diagnostic_payload(obj): + text = str(getattr(obj, "QetDiagnosticJson", "") or "").strip() + if not text: + return {}, False + try: + payload = json.loads(text) + except Exception: + return {}, True + return payload if isinstance(payload, dict) else {}, False + + +def _issue_codes_from_text(text): + codes = [] + for token in re.split(r"[,;,;\s]+", str(text or "")): + code = token.strip() + if code and code not in codes: + codes.append(code) + return codes + + +def _diagnostic_issue_codes(entry): + payload = entry.get("payload", {}) if isinstance(entry, dict) else {} + codes = [] + for code in _issue_codes_from_text(entry.get("issue_codes_text", "")): + if code not in codes: + codes.append(code) + if isinstance(payload, dict): + for code in list(payload.get("issue_codes", []) or []): + text = str(code or "").strip() + if text and text not in codes: + codes.append(text) + if entry.get("json_invalid") and "diagnostic_json_invalid" not in codes: + codes.append("diagnostic_json_invalid") + if entry.get("json_empty") and "diagnostic_json_empty" not in codes: + codes.append("diagnostic_json_empty") + return codes + + +def _routed_wire_diagnostic_gaps(doc, limit=8): + missing_samples = [] + invalid_samples = [] + missing_count = 0 + invalid_count = 0 + if doc is None: + return {"count": 0, "samples": [], "invalid_count": 0, "invalid_samples": []} + for obj in list(WiringObjects.iter_routed_wire_objects(doc)): + if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": + continue + diagnostic_text = str(getattr(obj, "QetRouteDiagnosticsJson", "") or "").strip() + if diagnostic_text: + try: + payload = json.loads(diagnostic_text) + if isinstance(payload, dict): + continue + except Exception: + pass + invalid_count += 1 + if len(invalid_samples) < int(limit or 0): + invalid_samples.append( + { + "name": str(getattr(obj, "Name", "") or ""), + "label": str(getattr(obj, "Label", "") or ""), + "wire_uuid": str(getattr(obj, "QetWireUuid", "") or ""), + "route_status": str(getattr(obj, "RouteStatus", "") or ""), + } + ) + continue + missing_count += 1 + if len(missing_samples) >= int(limit or 0): + continue + missing_samples.append( + { + "name": str(getattr(obj, "Name", "") or ""), + "label": str(getattr(obj, "Label", "") or ""), + "wire_uuid": str(getattr(obj, "QetWireUuid", "") or ""), + "route_status": str(getattr(obj, "RouteStatus", "") or ""), + } + ) return { - "max_lane_index": max_lane_index, - "spacing_mm": lane_spacing, - "max_offset_mm": lane_max_offset, + "count": missing_count, + "samples": missing_samples, + "invalid_count": invalid_count, + "invalid_samples": invalid_samples, } -def _route_track_min_capacity(route_track): - if not isinstance(route_track, dict): - return None - capacities = [] - for segment in route_track.get("segments", []) or []: - # 自动桥接段是虚拟连通边,不代表真实线槽截面,不能参与容量最小值计算。 - if isinstance(segment, dict) and bool(segment.get("is_bridge", False)): +def _routed_wire_issue_summary(doc, limit=8): + total_count = 0 + issue_wire_count = 0 + issue_code_counts = {} + samples = [] + if doc is None: + return { + "total_wire_count": 0, + "issue_wire_count": 0, + "issue_code_counts": {}, + "samples": [], + } + for obj in list(WiringObjects.iter_routed_wire_objects(doc)): + if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": continue - carrier = segment.get("carrier", {}) if isinstance(segment, dict) else {} - if not isinstance(carrier, dict): + total_count += 1 + issue_codes = _issue_codes_from_text(getattr(obj, "QetRouteIssueCodes", "")) + if not issue_codes: continue - try: - capacity = int(float(carrier.get("capacity", 0) or 0)) - except Exception: - capacity = 0 - if capacity > 0: - capacities.append(capacity) - if not capacities: - return None - return min(capacities) + issue_wire_count += 1 + for code in issue_codes: + issue_code_counts[code] = issue_code_counts.get(code, 0) + 1 + if len(samples) < int(limit or 0): + samples.append( + { + "name": str(getattr(obj, "Name", "") or ""), + "label": str(getattr(obj, "Label", "") or ""), + "wire_uuid": str(getattr(obj, "QetWireUuid", "") or ""), + "issue_codes": issue_codes, + "issue_labels": [_routing_diagnostic_issue_label(code) for code in issue_codes], + } + ) + return { + "total_wire_count": total_count, + "issue_wire_count": issue_wire_count, + "issue_code_counts": { + key: issue_code_counts[key] + for key in sorted(issue_code_counts) + }, + "samples": samples, + } + + +def _main_path_detour_missing_summary(doc, limit=8): + wire_count = 0 + rejected_labels = [] + seen_labels = set() + rejected_label_counts = {} + rejected_kind_counts = {} + current_route_source_label_counts = {} + bridge_pair_counts = {} + samples = [] + if doc is None: + return { + "wire_count": 0, + "rejected_fallback_labels": [], + "rejected_fallback_label_counts": {}, + "rejected_fallback_kind_counts": {}, + "current_route_source_label_counts": {}, + "bridge_pair_counts": {}, + "samples": [], + } + for obj in list(WiringObjects.iter_routed_wire_objects(doc)): + if (getattr(obj, "RouteType", "") or "").strip() != "RoutedConnection": + continue + issue_codes = _issue_codes_from_text(getattr(obj, "QetRouteIssueCodes", "")) + if "main_path_detour_missing" not in issue_codes: + continue + wire_count += 1 + sample = { + "name": str(getattr(obj, "Name", "") or ""), + "label": str(getattr(obj, "Label", "") or ""), + "wire_uuid": str(getattr(obj, "QetWireUuid", "") or ""), + "rejected_fallback_labels": [], + "rejected_fallback_kinds": [], + "current_route_source_labels": [], + } + diagnostic_text = str(getattr(obj, "QetRouteDiagnosticsJson", "") or "").strip() + if diagnostic_text: + try: + diagnostic = json.loads(diagnostic_text) + except Exception: + diagnostic = {} + reroute = diagnostic.get("selective_collision_reroute", {}) if isinstance(diagnostic, dict) else {} + if isinstance(reroute, dict): + for kind in list(reroute.get("rejected_fallback_kinds", []) or []): + kind = str(kind or "").strip() + if not kind: + continue + rejected_kind_counts[kind] = rejected_kind_counts.get(kind, 0) + 1 + if kind not in sample["rejected_fallback_kinds"]: + sample["rejected_fallback_kinds"].append(kind) + for label in list(reroute.get("rejected_fallback_labels", []) or []): + label = str(label or "").strip() + if not label: + continue + rejected_label_counts[label] = rejected_label_counts.get(label, 0) + 1 + if label not in seen_labels: + seen_labels.add(label) + rejected_labels.append(label) + if label not in sample["rejected_fallback_labels"]: + sample["rejected_fallback_labels"].append(label) + route_track_text = str(getattr(obj, "QetRouteTrackJson", "") or "").strip() + if route_track_text: + try: + route_track = json.loads(route_track_text) + except Exception: + route_track = {} + current_labels = _route_source_labels(route_track, limit=8) + sample["current_route_source_labels"] = current_labels + for label in current_labels: + current_route_source_label_counts[label] = current_route_source_label_counts.get(label, 0) + 1 + # 这里不自动建桥,只给出“兜底区域 -> 当前主路径”的人工补路方向。 + for rejected_label in sample["rejected_fallback_labels"]: + for current_label in current_labels: + pair_key = "{0} -> {1}".format(rejected_label, current_label) + bridge_pair_counts[pair_key] = bridge_pair_counts.get(pair_key, 0) + 1 + if len(samples) < int(limit or 0): + samples.append(sample) + return { + "wire_count": wire_count, + "rejected_fallback_labels": rejected_labels, + "rejected_fallback_label_counts": { + key: rejected_label_counts[key] + for key in sorted(rejected_label_counts) + }, + "rejected_fallback_kind_counts": { + key: rejected_kind_counts[key] + for key in sorted(rejected_kind_counts) + }, + "current_route_source_label_counts": { + key: current_route_source_label_counts[key] + for key in sorted(current_route_source_label_counts) + }, + "bridge_pair_counts": { + key: bridge_pair_counts[key] + for key in sorted(bridge_pair_counts) + }, + "samples": samples, + } + + +def _backfill_missing_endpoint_reason_context(sample, terminals, doc=None): + if not isinstance(sample, dict): + return sample + result = dict(sample) + for side in ("start", "end"): + if bool(result.get("{0}_found".format(side), False)): + continue + reason_code = str(result.get("{0}_missing_endpoint_reason_code".format(side), "") or "").strip() + reason_label = str(result.get("{0}_missing_endpoint_reason_label".format(side), "") or "").strip() + if reason_code or reason_label: + continue + _add_missing_endpoint_terminal_context(result, side, terminals, doc=doc) + return result + + +def _batch_missing_terminal_summary(batch_payload, doc=None): + if not isinstance(batch_payload, dict): + return { + "skipped_missing_terminal": 0, + "sample_wire_count": 0, + "missing_endpoint_count": 0, + "reason_code_counts": {}, + "reason_label_counts": {}, + "device_groups": [], + "samples": [], + } + skipped = _safe_int(batch_payload.get("skipped_missing_terminal", 0)) + samples = [] + reason_code_counts = {} + reason_label_counts = {} + device_groups = {} + missing_endpoint_count = 0 + terminals = index_terminals(doc) if doc is not None else {} + for sample in list(batch_payload.get("missing_endpoint_samples", []) or []): + if not isinstance(sample, dict): + continue + sample = _backfill_missing_endpoint_reason_context(sample, terminals, doc=doc) + samples.append(sample) + for side in ("start", "end"): + if sample.get("{0}_found".format(side)) is not False: + continue + terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + reason_code = str(sample.get("{0}_missing_endpoint_reason_code".format(side), "") or "").strip() + reason_label = str(sample.get("{0}_missing_endpoint_reason_label".format(side), "") or "").strip() + if not terminal_uuid and not reason_code and not reason_label: + continue + missing_endpoint_count += 1 + if terminal_uuid and not reason_code and not reason_label: + reason_code = "unknown" + reason_label = "缺端点原因未记录" + if reason_code: + reason_code_counts[reason_code] = reason_code_counts.get(reason_code, 0) + 1 + if reason_label: + reason_label_counts[reason_label] = reason_label_counts.get(reason_label, 0) + 1 + device_group_key = _missing_endpoint_device_group_key(sample, side) + group = device_groups.setdefault( + device_group_key, + { + "device_label": device_group_key[0], + "device_name": device_group_key[1], + "instance_id": device_group_key[2], + "element_uuid": device_group_key[3], + "missing_endpoint_count": 0, + "terminal_uuids": [], + "terminal_displays": [], + "reason_code_counts": {}, + "reason_label_counts": {}, + "wire_uuids": [], + "wire_labels": [], + }, + ) + group["missing_endpoint_count"] += 1 + _append_once(group["terminal_uuids"], terminal_uuid) + _append_once( + group["terminal_displays"], + str(sample.get("{0}_terminal_display".format(side), "") or "").strip(), + ) + _append_once(group["wire_uuids"], str(sample.get("wire_uuid", "") or "").strip()) + _append_once(group["wire_labels"], str(sample.get("wire_label", "") or "").strip()) + if reason_code: + group["reason_code_counts"][reason_code] = group["reason_code_counts"].get(reason_code, 0) + 1 + if reason_label: + group["reason_label_counts"][reason_label] = group["reason_label_counts"].get(reason_label, 0) + 1 + grouped_devices = sorted( + device_groups.values(), + key=lambda item: ( + -int(item.get("missing_endpoint_count", 0) or 0), + str(item.get("device_label", "") or ""), + str(item.get("device_name", "") or ""), + str(item.get("element_uuid", "") or ""), + ), + ) + return { + "skipped_missing_terminal": skipped, + "sample_wire_count": len(samples), + "missing_endpoint_count": missing_endpoint_count, + "reason_code_counts": { + key: reason_code_counts[key] + for key in sorted(reason_code_counts) + }, + "reason_label_counts": { + key: reason_label_counts[key] + for key in sorted(reason_label_counts) + }, + "device_groups": grouped_devices[:8], + "samples": samples[:8], + } + + +def _append_once(values, value): + text = str(value or "").strip() + if text and text not in values: + values.append(text) + + +def _missing_endpoint_device_group_key(sample, side): + device_label = str(sample.get("{0}_device_label".format(side), "") or "").strip() + device_name = str(sample.get("{0}_device_name".format(side), "") or "").strip() + instance_id = str(sample.get("{0}_instance_id".format(side), "") or "").strip() + element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() + display = device_label or device_name or instance_id or element_uuid or "未知设备" + return (display, device_name, instance_id, element_uuid) + + +def _missing_terminal_device_groups_text(groups, limit=3): + parts = [] + for group in list(groups or [])[: int(limit or 3)]: + if not isinstance(group, dict): + continue + label = str(group.get("device_label", "") or group.get("device_name", "") or "未知设备").strip() + count = _safe_int(group.get("missing_endpoint_count", 0)) + if not label or count <= 0: + continue + displays = [str(item or "").strip() for item in list(group.get("terminal_displays", []) or []) if str(item or "").strip()] + uuids = [str(item or "").strip() for item in list(group.get("terminal_uuids", []) or []) if str(item or "").strip()] + terminal_text = "、".join(displays[:3] or uuids[:3]) + if terminal_text: + parts.append("{0} 缺 {1} 处({2})".format(label, count, terminal_text)) + else: + parts.append("{0} 缺 {1} 处".format(label, count)) + return ",".join(parts) + + +def _routing_diagnostic_recommended_actions(summary): + if not isinstance(summary, dict): + return [] + actions = [] + + def add(action): + if action and action not in actions: + actions.append(action) + + missing_summary = summary.get("batch_missing_terminal_summary", {}) + if isinstance(missing_summary, dict) and _safe_int(missing_summary.get("skipped_missing_terminal", 0)) > 0: + reason_counts = missing_summary.get("reason_code_counts", {}) + if not isinstance(reason_counts, dict): + reason_counts = {} + if _safe_int(reason_counts.get("missing_device_binding_metadata", 0)) > 0: + add("检查 QET 导线端点是否提供 element_uuid 和 terminal_uuid(第一版不要求 start/end_instance_id)") + if _safe_int(reason_counts.get("device_not_in_3d_scene", 0)) > 0: + add("检查缺失 3D 设备是否已导入、装配并完成 2D/3D 绑定") + if ( + _safe_int(reason_counts.get("no_3d_terminals_for_element", 0)) > 0 + or _safe_int(reason_counts.get("no_3d_terminals_for_instance", 0)) > 0 + ): + add("点击“选择缺端子设备”定位需要补工程端子的设备") + if _safe_int(reason_counts.get("terminal_uuid_not_in_element", 0)) > 0: + add("点击“选择缺端子候选端子”核对 terminal_uuid 与脚号绑定") + if _safe_int(reason_counts.get("unknown", 0)) > 0: + add("重新生成布线连接,刷新缺端子原因诊断") + if not reason_counts: + add("点击“选择缺端子设备”定位需要补工程端子的设备") + + wire_issues = summary.get("routed_wire_issue_summary", {}) + issue_counts = wire_issues.get("issue_code_counts", {}) if isinstance(wire_issues, dict) else {} + if isinstance(wire_issues, dict) and _safe_int(wire_issues.get("issue_wire_count", 0)) > 0: + add("点击“选择异常导线”定位带问题码的导线") + if isinstance(issue_counts, dict) and ( + _safe_int(issue_counts.get("long_terminal_access", 0)) > 0 + or _safe_int(issue_counts.get("long_terminal_accesses", 0)) > 0 + ): + add("点击“选择长接入端子/设备”检查设备高度和局部出线路径") + if isinstance(issue_counts, dict) and ( + _safe_int(issue_counts.get("route_candidate_boundary_violations", 0)) > 0 + or _safe_int(issue_counts.get("boundary_warning", 0)) > 0 + ): + add("检查柜内边界和 UserPath,必要时补柜内主路径") + if isinstance(issue_counts, dict) and _safe_int(issue_counts.get("route_capacity_pressure", 0)) > 0: + add("检查路径容量,必要时补备用路径或提高线槽容量") + if isinstance(issue_counts, dict) and _safe_int(issue_counts.get("main_path_detour_missing", 0)) > 0: + add("点击“选择缺主路径导线”定位需要补 UserPath 或主路径桥接的导线") + + issue_codes = set(str(code or "").strip() for code in list(summary.get("issue_codes", []) or [])) + if "main_path_detour_missing" in issue_codes: + add("点击“选择缺主路径导线”定位需要补 UserPath 或主路径桥接的导线") + add("点击“选择缺主路径线路径”对照当前实际路径") + detour_summary = summary.get("main_path_detour_missing_summary", {}) + if isinstance(detour_summary, dict) and list(detour_summary.get("rejected_fallback_labels", []) or []): + add("点击“选择缺主路径补路位置”快速定位汇总需补区域") + add("选中缺主路径导线后点击“选择拒绝兜底路径”查看需补路径位置") + collision_count = 0 + batch_payload = ((summary.get("diagnostics", {}) or {}).get("RoutingConnectionBatch", {}) or {}).get("payload", {}) + if isinstance(batch_payload, dict): + collision_count = _safe_int(batch_payload.get("collision_warnings", 0)) + if ( + "collision_warnings" in issue_codes + or collision_count > 0 + or _safe_int(issue_counts.get("collision_warnings", 0)) > 0 + ): + collision_resolution = summary.get("batch_collision_resolution_summary", {}) + if isinstance(collision_resolution, dict): + counts = collision_resolution.get("counts", {}) + if isinstance(counts, dict): + structural_count = _safe_int(counts.get("review_pass_through_structural_obstacle", 0)) + device_count = _safe_int(counts.get("review_device_or_layout_collision", 0)) + if structural_count > 0: + add( + "先处理 {0} 个疑似结构件碰撞候选:确认后可标记 PassThrough".format( + structural_count + ) + ) + if device_count > 0: + add( + "另有 {0} 个疑似设备/装配碰撞需要补路径或调整装配".format( + device_count + ) + ) + top_obstacles = list(summary.get("batch_top_collision_obstacles", []) or []) + has_parent_refs = any( + isinstance(item, dict) + and (list(item.get("parent_names", []) or []) or list(item.get("parent_labels", []) or [])) + for item in top_obstacles + ) + if has_parent_refs: + add("点击“选择碰撞父装配”确认结构件后再标记忽略碰撞") + else: + add("点击“选择高发碰撞对象”和“选择碰撞导线”核对穿模位置") + return actions + + +def collect_routing_diagnostic_summary(doc): + """Collect the latest routing diagnostics into one FreeCAD-side summary.""" + project_uuid = _project_uuid(doc) if doc is not None else "" + summary = { + "project_uuid": project_uuid, + "ok": False, + "diagnostic_count": 0, + "diagnostics": {}, + "missing_diagnostic_kinds": list(_ROUTING_DIAGNOSTIC_KINDS), + "issue_codes": [], + "issue_labels": [], + "messages": [], + "runtime_version": "", + "batch_route_path_usage": {}, + "batch_route_status_counts": {}, + "batch_top_collision_obstacles": [], + "routed_wire_diagnostic_gaps": {"count": 0, "samples": []}, + "routed_wire_issue_summary": { + "total_wire_count": 0, + "issue_wire_count": 0, + "issue_code_counts": {}, + "samples": [], + }, + "main_path_detour_missing_summary": { + "wire_count": 0, + "rejected_fallback_labels": [], + "rejected_fallback_kind_counts": {}, + "samples": [], + }, + "batch_missing_terminal_summary": { + "skipped_missing_terminal": 0, + "sample_wire_count": 0, + "missing_endpoint_count": 0, + "reason_code_counts": {}, + "reason_label_counts": {}, + "samples": [], + }, + "recommended_actions": [], + } + if doc is None: + return summary + try: + group = doc.getObject("QETWiring_05_Diagnostics") + except Exception: + group = None + if group is None: + return summary + + diagnostics = {} + for obj in list(getattr(group, "Group", []) or []): + kind = str(getattr(obj, "QetDiagnosticKind", "") or "").strip() + if kind not in _ROUTING_DIAGNOSTIC_KINDS: + continue + diagnostic_json_text = str(getattr(obj, "QetDiagnosticJson", "") or "").strip() + payload, json_invalid = _read_diagnostic_payload(obj) + entry = { + "object_name": str(getattr(obj, "Name", "") or ""), + "label": str(getattr(obj, "Label", "") or ""), + "kind": kind, + "project_uuid": str(getattr(obj, "QetProjectUuid", "") or "").strip(), + "ok": bool(getattr(obj, "QetDiagnosticOk", False)), + "issue_codes_text": str(getattr(obj, "QetDiagnosticIssueCodes", "") or "").strip(), + "message": str(getattr(obj, "QetDiagnosticMessage", "") or "").strip(), + "payload": payload, + "json_invalid": json_invalid, + # 真实项目里可能残留旧版空诊断对象;汇总时要提醒用户重新运行对应检查。 + "json_empty": not diagnostic_json_text, + } + # 每类诊断只保留最新对象;正常流程会先清旧对象,这里只是兜底。 + diagnostics[kind] = entry + + issue_codes = [] + messages = [] + runtime_versions = {} + for kind in _ROUTING_DIAGNOSTIC_KINDS: + entry = diagnostics.get(kind) + if not entry: + continue + if not project_uuid and entry.get("project_uuid"): + project_uuid = entry.get("project_uuid", "") + if entry.get("message"): + messages.append(entry.get("message")) + payload = entry.get("payload") + if isinstance(payload, dict): + runtime_version = str(payload.get("runtime_version", "") or "").strip() + if runtime_version: + runtime_versions[kind] = runtime_version + for code in _diagnostic_issue_codes(entry): + if code not in issue_codes: + issue_codes.append(code) + + batch_payload = {} + batch_entry = diagnostics.get("RoutingConnectionBatch") + if isinstance(batch_entry, dict) and isinstance(batch_entry.get("payload"), dict): + batch_payload = batch_entry.get("payload") or {} + missing_kinds = [kind for kind in _ROUTING_DIAGNOSTIC_KINDS if kind not in diagnostics] + if batch_entry: + # “生成布线连接”是最终入口;它已经覆盖预检结果,不应因未单独点预检而判失败。 + missing_kinds = [kind for kind in missing_kinds if kind != "RoutingPreflight"] + embedded_network = batch_payload.get("routing_path_network_diagnostic") + if isinstance(embedded_network, dict): + # 批量报告内嵌路径网络诊断时,不再要求额外的 RoutingPathNetwork 独立对象。 + missing_kinds = [kind for kind in missing_kinds if kind != "RoutingPathNetwork"] + routed_wire_gaps = _routed_wire_diagnostic_gaps(doc) + routed_wire_issues = _routed_wire_issue_summary(doc) + missing_terminal_summary = _batch_missing_terminal_summary(batch_payload, doc=doc) + main_path_detour_missing = _main_path_detour_missing_summary(doc) + if _safe_int(missing_terminal_summary.get("skipped_missing_terminal", 0)) > 0: + if "missing_terminals" not in issue_codes: + issue_codes.append("missing_terminals") + if ( + _safe_int(missing_terminal_summary.get("missing_endpoint_count", 0)) > 0 + and "missing_endpoints" not in issue_codes + ): + issue_codes.append("missing_endpoints") + if _has_routing_error_status(batch_payload) and "routing_errors" not in issue_codes: + issue_codes.append("routing_errors") + if routed_wire_gaps.get("count", 0) > 0 and "routed_wire_diagnostics_missing" not in issue_codes: + issue_codes.append("routed_wire_diagnostics_missing") + if routed_wire_gaps.get("invalid_count", 0) > 0 and "routed_wire_diagnostics_invalid" not in issue_codes: + issue_codes.append("routed_wire_diagnostics_invalid") + if ( + _safe_int(main_path_detour_missing.get("wire_count", 0)) > 0 + and "main_path_detour_missing" not in issue_codes + ): + issue_codes.append("main_path_detour_missing") + all_present_ok = bool(diagnostics) and not missing_kinds and all( + bool(entry.get("ok", False)) for entry in diagnostics.values() + ) + summary.update( + { + "project_uuid": project_uuid, + "ok": all_present_ok and not issue_codes, + "diagnostic_count": len(diagnostics), + "diagnostics": diagnostics, + "missing_diagnostic_kinds": missing_kinds, + "issue_codes": issue_codes, + "issue_labels": [_routing_diagnostic_issue_label(code) for code in issue_codes], + "messages": messages, + # 批量布线最能代表本次生成结果;没有批量结果时再用预检/路径网络版本辅助排查。 + "runtime_version": ( + runtime_versions.get("RoutingConnectionBatch") + or runtime_versions.get("RoutingPreflight") + or runtime_versions.get("RoutingPathNetwork") + or "" + ), + "batch_route_path_usage": ( + dict(batch_payload.get("route_path_usage") or {}) + if isinstance(batch_payload.get("route_path_usage"), dict) + else {} + ), + "batch_route_status_counts": _route_status_counts_payload(batch_payload), + "batch_top_collision_obstacles": ( + list(batch_payload.get("top_collision_obstacles") or []) + if isinstance(batch_payload.get("top_collision_obstacles"), list) + else [] + ), + "batch_collision_resolution_summary": ( + dict(batch_payload.get("collision_resolution_summary") or {}) + if isinstance(batch_payload.get("collision_resolution_summary"), dict) + else {} + ), + "routed_wire_diagnostic_gaps": routed_wire_gaps, + "routed_wire_issue_summary": routed_wire_issues, + "main_path_detour_missing_summary": main_path_detour_missing, + "batch_missing_terminal_summary": missing_terminal_summary, + } + ) + summary["recommended_actions"] = _routing_diagnostic_recommended_actions(summary) + return summary + + +def format_routing_diagnostic_summary(summary): + if not isinstance(summary, dict): + return "汇总诊断失败:诊断结果无效。" + status = "通过" if summary.get("ok") else "未通过" + message = "汇总诊断:{0},诊断对象 {1} 个。".format( + status, + _safe_int(summary.get("diagnostic_count", 0)), + ) + missing = list(summary.get("missing_diagnostic_kinds", []) or []) + if missing: + message += " 未生成:{0}。".format("、".join(missing)) + labels = list(summary.get("issue_labels", []) or []) + if labels: + message += " 问题:{0}。".format("、".join(labels[:8])) + status_text = _route_status_counts_text(summary.get("batch_route_status_counts", {})) + if status_text: + message += "\n结果状态:{0}。".format(status_text) + runtime_version = str(summary.get("runtime_version", "") or "").strip() + if runtime_version: + message += " 运行版本:{0}。".format(runtime_version) + path_usage = summary.get("batch_route_path_usage", {}) + if isinstance(path_usage, dict): + main_path_routes = _safe_int(path_usage.get("main_path_routes", 0)) + fallback_routes = _safe_int(path_usage.get("fallback_routes", 0)) + if main_path_routes > 0 or fallback_routes > 0: + message += "\n路径采用:线槽/主路径 {0} 条,布线面/辅助路径 {1} 条。".format( + main_path_routes, + fallback_routes, + ) + wire_issues = summary.get("routed_wire_issue_summary", {}) + if isinstance(wire_issues, dict): + issue_wire_count = _safe_int(wire_issues.get("issue_wire_count", 0)) + total_wire_count = _safe_int(wire_issues.get("total_wire_count", 0)) + if issue_wire_count > 0: + message += "\n异常导线:{0}/{1} 条".format(issue_wire_count, total_wire_count) + counts = wire_issues.get("issue_code_counts", {}) + if isinstance(counts, dict) and counts: + items = [] + for code, count in sorted(counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0])))[:5]: + label = _routing_diagnostic_issue_label(code) + count_value = _safe_int(count) + if label and count_value > 0: + items.append("{0} {1} 条".format(label, count_value)) + if items: + message += "({0})".format("、".join(items)) + message += "。" + detour_summary = summary.get("main_path_detour_missing_summary", {}) + if isinstance(detour_summary, dict): + detour_count = _safe_int(detour_summary.get("wire_count", 0)) + if detour_count > 0: + message += "\n缺主路径绕行:{0} 条".format(detour_count) + label_counts = detour_summary.get("rejected_fallback_label_counts", {}) + location_items = [] + if isinstance(label_counts, dict) and label_counts: + for label, count in sorted(label_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0])))[:5]: + label_text = str(label or "").strip() + count_value = _safe_int(count) + if label_text and count_value > 0: + location_items.append("{0} {1} 条".format(label_text, count_value)) + if not location_items: + labels = [ + str(label or "").strip() + for label in list(detour_summary.get("rejected_fallback_labels", []) or []) + if str(label or "").strip() + ] + location_items = labels[:5] + if location_items: + message += ",需补路径位置:{0}".format("、".join(location_items)) + bridge_pair_text = _main_path_detour_bridge_pair_text(detour_summary) + if bridge_pair_text: + message += ";补路配对:{0}".format(bridge_pair_text) + message += "。" + missing_terminal_summary = summary.get("batch_missing_terminal_summary", {}) + if isinstance(missing_terminal_summary, dict): + skipped_missing = _safe_int(missing_terminal_summary.get("skipped_missing_terminal", 0)) + if skipped_missing > 0: + message += "\n缺端子:{0} 条".format(skipped_missing) + label_counts = missing_terminal_summary.get("reason_label_counts", {}) + if isinstance(label_counts, dict) and label_counts: + items = [] + for label, count in sorted(label_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0])))[:5]: + label_text = str(label or "").strip() + count_value = _safe_int(count) + if label_text and count_value > 0: + items.append("{0} {1} 处".format(label_text, count_value)) + if items: + message += "({0})".format("、".join(items)) + message += "。" + device_groups_text = _missing_terminal_device_groups_text( + missing_terminal_summary.get("device_groups", []) + ) + if device_groups_text: + message += "\n需补端子设备:{0}。".format(device_groups_text) + actions = [str(item or "").strip() for item in list(summary.get("recommended_actions", []) or []) if str(item or "").strip()] + if actions: + message += "\n建议:{0}。".format(";".join(actions[:5])) + top_collision_obstacles = [] + for item in list(summary.get("batch_top_collision_obstacles", []) or [])[:3]: + if not isinstance(item, dict): + continue + label = str(item.get("label", "") or "").strip() + count = _safe_int(item.get("count", 0)) + if label and count > 0: + top_collision_obstacles.append(item) + if top_collision_obstacles: + message += "\n碰撞高发对象:{0}。".format( + _top_collision_obstacle_items_text(top_collision_obstacles) + ) + collision_resolution = summary.get("batch_collision_resolution_summary", {}) + if isinstance(collision_resolution, dict): + resolution_action = str(collision_resolution.get("recommended_action", "") or "").strip() + if resolution_action: + message += "\n碰撞分类建议:{0}".format(resolution_action) + messages = [str(item or "").strip() for item in list(summary.get("messages", []) or []) if str(item or "").strip()] + if messages: + message += "\n最近诊断:{0}".format("\n".join(messages[:3])) + wire_gaps = summary.get("routed_wire_diagnostic_gaps", {}) + if isinstance(wire_gaps, dict): + gap_count = _safe_int(wire_gaps.get("count", 0)) + if gap_count > 0: + message += "\n导线诊断缺失:{0} 条".format(gap_count) + samples = list(wire_gaps.get("samples", []) or []) + if samples: + sample = samples[0] + sample_label = str(sample.get("label", "") or sample.get("name", "") or "").strip() + if sample_label: + message += ",示例 {0}".format(sample_label) + message += "。" + invalid_count = _safe_int(wire_gaps.get("invalid_count", 0)) + if invalid_count > 0: + message += "\n导线诊断 JSON 无效:{0} 条".format(invalid_count) + samples = list(wire_gaps.get("invalid_samples", []) or []) + if samples: + sample = samples[0] + sample_label = str(sample.get("label", "") or sample.get("name", "") or "").strip() + if sample_label: + message += ",示例 {0}".format(sample_label) + message += "。" + return message -def _route_capacity_pressure_summary(report): - pressure = {} - for route in report.get("routes", []) or []: - if not isinstance(route, dict): - continue - lane = route.get("lane", {}) - if not isinstance(lane, dict): +def _clear_routing_diagnostic_summary_objects(doc): + group = WiringObjects.ensure_diagnostic_group(doc, _project_uuid(doc)) + removed = 0 + for obj in list(getattr(group, "Group", []) or []): + if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "RoutingDiagnosticSummary": continue try: - max_parallel_wires = int(lane.get("index", 0) or 0) + 1 + group.removeObject(obj) except Exception: - max_parallel_wires = 1 - route_capacity = _route_track_min_capacity(route.get("route_track", {})) - if route_capacity is None or max_parallel_wires <= route_capacity: - continue - if not pressure or max_parallel_wires > pressure.get("max_parallel_wires", 0): - pressure = { - "max_parallel_wires": max_parallel_wires, - "min_capacity": route_capacity, - } - return pressure + try: + group.Group = [ + candidate + for candidate in list(getattr(group, "Group", []) or []) + if candidate is not obj + ] + except Exception: + pass + try: + if doc.getObject(getattr(obj, "Name", "")) is not None: + doc.removeObject(obj.Name) + removed += 1 + except Exception: + pass + return removed -_ROUTE_QUALITY_WARNING_KIND_LABELS = { - "RoutingRange": "布线面", - "AuxiliaryPath": "辅助路径", -} +def write_routing_diagnostic_summary(doc, summary=None): + if doc is None: + return None + payload = summary if isinstance(summary, dict) else collect_routing_diagnostic_summary(doc) + project_uuid = str(payload.get("project_uuid", "") or _project_uuid(doc)).strip() + group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) + _clear_routing_diagnostic_summary_objects(doc) + # 汇总对象用于手动测试复盘:把三类诊断的最新状态固定到树目录里。 + diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETRoutingDiagnosticSummary")) + diagnostic.Label = "QET Routing Diagnostic Summary" + _set_string(diagnostic, "QetDiagnosticKind", "RoutingDiagnosticSummary", "QET diagnostic kind") + _set_string(diagnostic, "QetProjectUuid", project_uuid, "Project UUID") + _set_bool(diagnostic, "QetDiagnosticOk", bool(payload.get("ok", False)), "QET diagnostic pass state") + _set_string( + diagnostic, + "QetDiagnosticIssueCodes", + _diagnostic_issue_codes_text(payload.get("issue_codes", [])), + "QET routing diagnostic issue codes", + ) + _set_string( + diagnostic, + "QetDiagnosticIssueLabels", + _diagnostic_issue_labels_text(payload.get("issue_codes", [])), + "QET routing diagnostic issue labels", + ) + _set_string( + diagnostic, + "QetDiagnosticMessage", + format_routing_diagnostic_summary(payload), + "QET routing diagnostic summary message", + ) + _set_string( + diagnostic, + "QetDiagnosticJson", + json.dumps(payload, ensure_ascii=False), + "QET routing diagnostic summary payload", + ) + group.addObject(diagnostic) + return diagnostic -def _route_track_carrier_kinds(route_track): - if not isinstance(route_track, dict): - return {} - counts = {} - has_segment_list = isinstance(route_track.get("segments"), list) - raw_segments = route_track.get("segments", []) - segments = raw_segments or [] - for segment in segments: - # 虚拟桥接段只表示网络连通,不代表导线真实经过该类型 carrier。 - 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): +def _clear_routing_connection_batch_diagnostics(doc): + group = WiringObjects.ensure_diagnostic_group(doc, _project_uuid(doc)) + removed = 0 + for obj in list(getattr(group, "Group", []) or []): + if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "RoutingConnectionBatch": continue - kind = str(carrier.get("kind", "") or "").strip() - if kind: - counts[kind] = counts.get(kind, 0) + 1 - if counts: - return counts - if has_segment_list: - return {} - carrier_kinds = route_track.get("carrier_kinds", {}) - if isinstance(carrier_kinds, dict) and carrier_kinds: - return { - str(key): value - for key, value in carrier_kinds.items() - if str(key).strip() - } - return {} + try: + group.removeObject(obj) + except Exception: + try: + group.Group = [ + candidate + for candidate in list(getattr(group, "Group", []) or []) + if candidate is not obj + ] + except Exception: + pass + try: + if doc.getObject(getattr(obj, "Name", "")) is not None: + doc.removeObject(obj.Name) + removed += 1 + except Exception: + pass + return removed -def _route_track_carrier_names(route_track, limit=8): +def _compact_route_sample(route): + route_track = route.get("route_track", {}) if isinstance(route, dict) else {} if not isinstance(route_track, dict): - return [] - names = [] - seen = set() - has_segment_list = isinstance(route_track.get("segments"), list) - for segment in route_track.get("segments", []) or []: - # 诊断样例只列真实经过的 carrier;虚拟桥接段不显示为源路径对象。 - 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 - name = str(carrier.get("name", "") or "").strip() - if not name or name in seen: - continue - seen.add(name) - names.append(name) - if len(names) >= int(limit or 0): - return names - if has_segment_list: - return names - return list(route_track.get("carrier_names", []) or [])[: int(limit or 0)] - - -def _route_quality_warning_summary(report): - warning_count = 0 - sample = None - for warning in _route_quality_warning_samples(report, limit=0): - warning_labels = list(warning.get("carrier_labels", []) or []) - if not warning_labels: - continue - warning_count += 1 - if sample is None: - sample = { - "wire": warning.get("wire_label") or warning.get("wire_uuid") or "未知导线", - "labels": warning_labels, - } - if warning_count <= 0: - return {} - return { - "count": warning_count, - "sample": sample or {}, + route_track = {} + access_payload = _route_access_payload(route) + collision_payload = _route_collision_payload(route.get("collisions", [])) + quality_payload = _route_quality_payload(route_track) + lane_capacity_payload = _route_lane_capacity_payload(route) + boundary_payload = _route_boundary_payload(route) + issue_codes = _route_issue_codes(route, route.get("collisions", [])) + sample = { + "wire_uuid": route.get("wire_uuid", ""), + "wire_label": route.get("wire_label", ""), + "wire_object_label": route.get("wire_object_label", ""), + "start_terminal_uuid": route.get("start_terminal_uuid", ""), + "start_element_uuid": route.get("start_element_uuid", ""), + "start_terminal_display": route.get("start_terminal_display", ""), + "end_terminal_uuid": route.get("end_terminal_uuid", ""), + "end_element_uuid": route.get("end_element_uuid", ""), + "end_terminal_display": route.get("end_terminal_display", ""), + "endpoint_label": route.get("endpoint_label", ""), + "route_status": route.get("route_status", ""), + "wire_style_id": route.get("wire_style_id", ""), + "wire_style_status": route.get("wire_style_status", ""), + "length_mm": route.get("length_mm", 0.0), + "lane": route.get("lane", {}), + "algorithm": route.get("algorithm", ""), + "collision_count": route.get("collision_count", 0), + "carrier_kinds": _route_track_carrier_kinds(route_track), + "carrier_names": _route_track_carrier_names(route_track, limit=8), + "route_source_labels": _route_source_labels(route_track, limit=8), + "access": access_payload, + "collision_summary": collision_payload, + "quality": quality_payload, + "issue_codes": issue_codes, + "issue_labels": [ + _routing_diagnostic_issue_label(code) + for code in issue_codes + ], } + if lane_capacity_payload["capacity_status"]: + sample["capacity"] = lane_capacity_payload + if boundary_payload["boundary_aware"]: + sample["boundary"] = boundary_payload + selective_status = str(route.get("selective_collision_reroute_status", "") or "").strip() + if selective_status: + sample["selective_collision_reroute"] = { + "status": selective_status, + "rejected_fallback_kinds": list( + route.get("selective_collision_reroute_rejected_fallback_kinds", []) or [] + ), + "rejected_fallback_labels": list( + route.get("selective_collision_reroute_rejected_fallback_labels", []) or [] + ), + } + if isinstance(route.get("wire_style"), dict) and route.get("wire_style"): + sample["wire_style"] = dict(route.get("wire_style") or {}) + network = route.get("network", {}) + if isinstance(network, dict): + bridged_segments = network.get("bridged_segments", 0) + if "bridged_segments" in route_track: + bridged_segments = route_track.get("bridged_segments", 0) + sample["network"] = { + "carriers": network.get("carriers", 0), + "segments": network.get("segments", 0), + "bridged_segments": bridged_segments, + "blocked_segments": network.get("blocked_segments", 0), + "entry_distance": network.get("entry_distance", 0.0), + "exit_distance": network.get("exit_distance", 0.0), + "entry_candidate_rank": network.get("entry_candidate_rank", 1), + "exit_candidate_rank": network.get("exit_candidate_rank", 1), + "entry_candidate_score": network.get("entry_candidate_score", 0.0), + "route_candidate_obstacle_hits": network.get("route_candidate_obstacle_hits", 0), + "boundary_aware": bool(network.get("boundary_aware", False)), + "route_candidate_boundary_violations": network.get( + "route_candidate_boundary_violations", 0 + ), + "route_constraints": network.get("route_constraints", {}), + } + return sample -def _route_quality_warning_samples(report, limit=8): - samples = [] - max_samples = int(limit or 0) - for route in report.get("routes", []) or []: - if not isinstance(route, dict): - continue - carrier_kinds = _route_track_carrier_kinds(route.get("route_track", {})) - warning_kinds = [ - kind - for kind in _ROUTE_QUALITY_WARNING_KIND_LABELS - if carrier_kinds.get(kind, 0) - ] - if not warning_kinds: - continue - if max_samples <= 0 or len(samples) < max_samples: - samples.append( - { - "wire_uuid": route.get("wire_uuid", ""), - "wire_label": route.get("wire_label", ""), - "start_terminal_uuid": route.get("start_terminal_uuid", ""), - "end_terminal_uuid": route.get("end_terminal_uuid", ""), - "carrier_kinds": warning_kinds, - "carrier_labels": [ - _ROUTE_QUALITY_WARNING_KIND_LABELS.get(kind, kind) - for kind in warning_kinds - ], - } - ) - return samples +def _route_sample_priority(route, index): + if not isinstance(route, dict): + return (1, index) + issue_count = len(_route_issue_codes(route, route.get("collisions", []))) + if issue_count <= 0: + return (1, index) + return (0, -issue_count, index) -def _long_network_entry_warning_samples(report, limit=8): - try: - warning_distance = float( - report.get( - "terminal_access_warning_distance", - RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE, - ) - or 0.0 - ) - except Exception: - warning_distance = float(RoutingNetwork.DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE) - if warning_distance <= 0.0: +def _routing_connection_batch_issue_codes(report): + if not isinstance(report, dict): return [] + collision_resolution_counts = _collision_resolution_counts(report) + collision_relation_counts = _collision_relation_counts(report) + missing_endpoint_reason_counts = _missing_endpoint_reason_counts_from_samples( + report.get("missing_endpoint_samples", []) + ) + checks = ( + ("no_wire_tasks", _safe_int(report.get("total_wires", 0)) <= 0), + ("no_routed_connections", _has_routing_attempt_without_results(report)), + ("missing_terminals", _safe_int(report.get("skipped_missing_terminal", 0)) > 0), + ( + "missing_device_binding_metadata", + _safe_int(missing_endpoint_reason_counts.get("missing_device_binding_metadata", 0)) > 0, + ), + ( + "device_not_in_3d_scene", + _safe_int(missing_endpoint_reason_counts.get("device_not_in_3d_scene", 0)) > 0, + ), + ( + "no_3d_terminals_for_element", + _safe_int(missing_endpoint_reason_counts.get("no_3d_terminals_for_element", 0)) > 0, + ), + ( + "no_3d_terminals_for_instance", + _safe_int(missing_endpoint_reason_counts.get("no_3d_terminals_for_instance", 0)) > 0, + ), + ( + "terminal_uuid_not_in_element", + _safe_int(missing_endpoint_reason_counts.get("terminal_uuid_not_in_element", 0)) > 0, + ), + ( + "missing_route_network", + _safe_int(report.get("skipped_missing_route_network", 0)) > 0, + ), + ("routing_errors", _has_routing_error_status(report)), + ("collision_warnings", _safe_int(report.get("collision_warnings", 0)) > 0), + ( + "structural_collision_candidates", + _safe_int(collision_resolution_counts.get("review_pass_through_structural_obstacle", 0)) > 0, + ), + ( + "device_or_layout_collisions", + _safe_int(collision_resolution_counts.get("review_device_or_layout_collision", 0)) > 0, + ), + ( + "third_party_device_collisions", + _safe_int(collision_relation_counts.get("third_party_device_collision", 0)) > 0, + ), + ( + "endpoint_device_collisions", + _safe_int(collision_relation_counts.get("endpoint_device_collision", 0)) > 0, + ), + ( + "main_path_detour_missing", + _safe_int(report.get("selective_collision_reroute_rejected_fallback", 0)) > 0, + ), + ("missing_wire_styles", bool(_wire_style_status_samples(report, status="Missing", limit=1))), + ( + "route_quality_warnings", + bool(_route_quality_warning_samples(report, limit=1)), + ), + ("main_path_not_used", _main_path_not_used(report)), + ( + "long_terminal_access", + bool(_long_network_entry_warning_samples(report, limit=1)), + ), + ( + "route_candidate_obstacle_hits", + bool(_route_candidate_obstacle_warning_samples(report, limit=1)), + ), + ( + "route_candidate_boundary_violations", + bool(_route_candidate_boundary_warning_samples(report, limit=1)), + ), + ( + "route_capacity_pressure", + bool(_route_capacity_pressure_samples(report, limit=1)), + ), + ) + return [code for code, enabled in checks if enabled] + +def _routed_route_issue_summary_from_report(report): + issue_counts = {} + issue_wire_count = 0 + total_wire_count = 0 samples = [] - max_samples = int(limit or 0) - for route in report.get("routes", []) or []: + if not isinstance(report, dict): + return { + "issue_wire_count": 0, + "total_wire_count": 0, + "issue_code_counts": {}, + "samples": [], + } + for route in list(report.get("routes", []) or []): if not isinstance(route, dict): continue - network = route.get("network", {}) - if not isinstance(network, dict): - continue - long_parts = [] - warning_sides = [] - for side, label, key in ( - ("entry", "起点", "entry_distance"), - ("exit", "终点", "exit_distance"), - ): - try: - distance = float(network.get(key, 0.0) or 0.0) - except Exception: - distance = 0.0 - if distance > warning_distance: - warning_sides.append(side) - long_parts.append("{0}接入 {1:.1f} mm".format(label, distance)) - if not long_parts: + total_wire_count += 1 + codes = [ + str(code or "").strip() + for code in list(route.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if not codes: continue - if max_samples <= 0 or len(samples) < max_samples: + issue_wire_count += 1 + for code in codes: + issue_counts[code] = issue_counts.get(code, 0) + 1 + if len(samples) < 8: samples.append( { "wire_uuid": route.get("wire_uuid", ""), - "wire_label": route.get("wire_label", ""), - "wire": _wire_sample_text(route), - "start_terminal_uuid": route.get("start_terminal_uuid", ""), - "end_terminal_uuid": route.get("end_terminal_uuid", ""), - "entry_distance": float(network.get("entry_distance", 0.0) or 0.0), - "exit_distance": float(network.get("exit_distance", 0.0) or 0.0), - "warning_sides": warning_sides, - "warning_parts": long_parts, - "warning_distance": float(warning_distance), + "label": route.get("wire_object_label", "") or route.get("wire_label", ""), + "issue_codes": codes, } ) - return samples + return { + "issue_wire_count": issue_wire_count, + "total_wire_count": total_wire_count, + "issue_code_counts": dict(sorted(issue_counts.items())), + "samples": samples, + } -def _long_network_entry_summary(report): - samples = _long_network_entry_warning_samples(report, limit=1) - if not samples: - return {} +def _main_path_detour_missing_summary_from_report(report, limit=8): + # 批量布线刚完成时,测试或某些运行路径可能还没把导线对象完整挂入分组; + # 这里直接从本次 routes[] 生成同一份补路摘要,避免面板报告漏掉缺主路径位置。 + wire_count = 0 + rejected_labels = [] + seen_labels = set() + rejected_label_counts = {} + rejected_kind_counts = {} + current_route_source_label_counts = {} + bridge_pair_counts = {} + samples = [] + if not isinstance(report, dict): + return { + "wire_count": 0, + "rejected_fallback_labels": [], + "rejected_fallback_label_counts": {}, + "rejected_fallback_kind_counts": {}, + "current_route_source_label_counts": {}, + "bridge_pair_counts": {}, + "samples": [], + } + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + issue_codes = [ + str(code or "").strip() + for code in list(route.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if "main_path_detour_missing" not in issue_codes: + continue + wire_count += 1 + sample = { + "name": "", + "label": str(route.get("wire_object_label", "") or route.get("wire_label", "") or ""), + "wire_uuid": str(route.get("wire_uuid", "") or ""), + "rejected_fallback_labels": [], + "rejected_fallback_kinds": [], + "current_route_source_labels": [], + } + for kind in list(route.get("selective_collision_reroute_rejected_fallback_kinds", []) or []): + kind = str(kind or "").strip() + if not kind: + continue + rejected_kind_counts[kind] = rejected_kind_counts.get(kind, 0) + 1 + if kind not in sample["rejected_fallback_kinds"]: + sample["rejected_fallback_kinds"].append(kind) + for label in list(route.get("selective_collision_reroute_rejected_fallback_labels", []) or []): + label = str(label or "").strip() + if not label: + continue + rejected_label_counts[label] = rejected_label_counts.get(label, 0) + 1 + if label not in seen_labels: + seen_labels.add(label) + rejected_labels.append(label) + if label not in sample["rejected_fallback_labels"]: + sample["rejected_fallback_labels"].append(label) + current_labels = _route_source_labels(route.get("route_track", {}), limit=8) + sample["current_route_source_labels"] = current_labels + for label in current_labels: + current_route_source_label_counts[label] = current_route_source_label_counts.get(label, 0) + 1 + for rejected_label in sample["rejected_fallback_labels"]: + for current_label in current_labels: + pair_key = "{0} -> {1}".format(rejected_label, current_label) + bridge_pair_counts[pair_key] = bridge_pair_counts.get(pair_key, 0) + 1 + if len(samples) < int(limit or 0): + samples.append(sample) return { - "count": len(_long_network_entry_warning_samples(report, limit=0)), - "sample": samples[0], - "warning_distance": float(samples[0].get("warning_distance", 0.0) or 0.0), + "wire_count": wire_count, + "rejected_fallback_labels": rejected_labels, + "rejected_fallback_label_counts": { + key: rejected_label_counts[key] + for key in sorted(rejected_label_counts) + }, + "rejected_fallback_kind_counts": { + key: rejected_kind_counts[key] + for key in sorted(rejected_kind_counts) + }, + "current_route_source_label_counts": { + key: current_route_source_label_counts[key] + for key in sorted(current_route_source_label_counts) + }, + "bridge_pair_counts": { + key: bridge_pair_counts[key] + for key in sorted(bridge_pair_counts) + }, + "samples": samples, } -def format_eplan_connection_route_report(report): - message = "批量生成布线连接完成:routed={0}, collision_warnings={1}, missing_terminals={2}".format( - report.get("routed", 0), - report.get("collision_warnings", 0), - report.get("skipped_missing_terminal", 0), - ) - status_counts = report.get("route_status_counts", {}) - if isinstance(status_counts, dict) and status_counts: - status_labels = { - "Routed": "正常", - "CollisionWarning": "碰撞告警", - "Error": "错误", - "MissingTerminal": "缺失端子", - "MissingRouteNetwork": "缺少布线路径网络", - "Invalid": "无效任务", - } - def status_count_value(value): - try: - return int(value or 0) - except Exception: - return 0 - status_parts = [] - for key in ( - "Routed", - "CollisionWarning", - "Error", - "MissingTerminal", - "MissingRouteNetwork", - "Invalid", +def _attach_main_path_detour_report_summary(doc, report): + if not isinstance(report, dict): + return report + detour_summary = _main_path_detour_missing_summary_from_report(report) + if _safe_int(detour_summary.get("wire_count", 0)) <= 0: + detour_summary = _main_path_detour_missing_summary(doc) + report["main_path_detour_missing_summary"] = detour_summary + action_summary = { + "issue_codes": list(report.get("issue_codes", []) or []), + "main_path_detour_missing_summary": detour_summary, + "routed_wire_issue_summary": _routed_route_issue_summary_from_report(report), + "batch_top_collision_obstacles": list(report.get("top_collision_obstacles", []) or []), + "batch_collision_resolution_summary": _collision_resolution_summary(report), + "diagnostics": { + "RoutingConnectionBatch": { + "payload": report, + } + }, + } + report["recommended_actions"] = _routing_diagnostic_recommended_actions(action_summary) + return report + + +def _find_route_bridge_sources_by_name_or_label(doc, name="", label=""): + refs = [] + seen_names = set() + name = str(name or "").strip() + label = str(label or "").strip() + if doc is None: + return refs + if name: + obj = doc.getObject(name) + if obj is not None: + refs.append(obj) + seen_names.add(getattr(obj, "Name", "")) + for candidate in list(getattr(doc, "Objects", []) or []): + candidate_name = getattr(candidate, "Name", "") + if candidate_name in seen_names: + continue + candidate_label = str(getattr(candidate, "Label", "") or "").strip() + route_source_name = str(getattr(candidate, "QetRouteSourceName", "") or "").strip() + route_source_label = str(getattr(candidate, "QetRouteSourceLabel", "") or "").strip() + if ( + (label and (candidate_label == label or route_source_label == label)) + or (name and (candidate_name == name or route_source_name == name)) ): - value = status_count_value(status_counts.get(key, 0)) - if value > 0: - status_parts.append("{0} {1} 条".format(status_labels[key], value)) - for key, value in sorted(status_counts.items()): - value = status_count_value(value) - if key in status_labels or value <= 0: - continue - status_parts.append("{0} {1} 条".format(key, value)) - if status_parts: - message += "\n结果状态:{0}。".format(",".join(status_parts)) - prepared_layout = report.get("prepared_layout") - if isinstance(prepared_layout, dict): - message += "\n布线布局空间:线槽路径 {0} 条,布线面 {1} 条,端子接入 {2} 条。".format( - prepared_layout.get("wire_duct_carriers", 0), - prepared_layout.get("surface_carriers", 0), - prepared_layout.get("terminal_access_carriers", 0), - ) - 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 = [ - _routing_path_network_issue_label(code) - for code in list(path_diagnostic.get("issue_codes", []) or [])[:3] - ] - message += "\n路径网络检查提示:{0}。".format("、".join(issue_labels) if issue_labels else "存在问题") - if report.get("skipped_missing_route_network", 0) > 0: - message += "\n缺少布线路径网络:{0} 条导线已跳过。请先生成线槽、布线面或布线路径网络。".format( - report.get("skipped_missing_route_network", 0) - ) - route_sample = (report.get("missing_route_network_samples") or [None])[0] - if route_sample: - message += "\n缺路径网络示例:导线 {0},{1}。".format( - _wire_sample_text(route_sample), - _endpoint_pair_text(route_sample), - ) - # 示例带上原始失败原因,手动测试时可以直接判断是空网络、端点不连通还是距离阈值限制。 - route_error = str(route_sample.get("error", "") or "").strip() - if route_error: - message += "原因:{0}。".format(route_error) - total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0) - if total_length_mm > 0.0: - message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm) - replaced_routed_connections = int(report.get("replaced_routed_connections", 0) or 0) - if replaced_routed_connections > 0: - message += "\n已替换旧布线连接:{0} 条。".format(replaced_routed_connections) - hidden_route_carriers = int(report.get("hidden_route_carriers", 0) or 0) - if hidden_route_carriers > 0: - message += "\n已隐藏走线路径辅助对象:{0} 条。".format(hidden_route_carriers) - bridged_segments = _route_network_metric_max(report, "bridged_segments") - blocked_segments = _route_network_metric_max(report, "blocked_segments") - network_parts = [] - if bridged_segments > 0: - network_parts.append("自动桥接 {0} 段相邻/投影主路径".format(bridged_segments)) - if blocked_segments > 0: - network_parts.append("避障屏蔽 {0} 段".format(blocked_segments)) - if network_parts: - message += "\n路径网络:{0}。".format(",".join(network_parts)) - lane_summary = _route_lane_summary(report) - if lane_summary: - lane_text = "并行错位:最大 lane {0},间距 {1:.1f} mm".format( - lane_summary.get("max_lane_index", 0), - float(lane_summary.get("spacing_mm", 0.0) or 0.0), - ) - max_offset = float(lane_summary.get("max_offset_mm", 0.0) or 0.0) - if max_offset > 0.0: - lane_text += ",最大偏移 {0:.1f} mm".format(max_offset) - message += "\n{0}。".format(lane_text) - capacity_pressure = _route_capacity_pressure_summary(report) - if capacity_pressure: - message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}。".format( - capacity_pressure.get("max_parallel_wires", 0), - capacity_pressure.get("min_capacity", 0), - ) - candidate_ranks = [] - for route in report.get("routes", []) or []: - if not isinstance(route, dict): + refs.append(candidate) + seen_names.add(candidate_name) + return refs + + +def _create_main_path_detour_bridges_from_report(doc, report, project_uuid=""): + detour_summary = report.get("main_path_detour_missing_summary", {}) if isinstance(report, dict) else {} + pair_counts = detour_summary.get("bridge_pair_counts", {}) if isinstance(detour_summary, dict) else {} + if not isinstance(pair_counts, dict): + pair_counts = {} + + created = [] + missing_pairs = [] + duplicates = 0 + for pair_text, _count in sorted(pair_counts.items(), key=lambda item: (-_safe_int(item[1]), str(item[0]))): + pair_text = str(pair_text or "").strip() + if " -> " not in pair_text: continue - network = route.get("network", {}) - if not isinstance(network, dict): + left_label, right_label = [part.strip() for part in pair_text.split(" -> ", 1)] + if not left_label or not right_label: continue - try: - entry_rank = int(network.get("entry_candidate_rank", 0) or 0) - except Exception: - entry_rank = 0 - try: - exit_rank = int(network.get("exit_candidate_rank", 0) or 0) - except Exception: - exit_rank = 0 - if entry_rank > 1 or exit_rank > 1: - candidate_ranks.append((entry_rank, exit_rank)) - if candidate_ranks: - entry_rank, exit_rank = candidate_ranks[0] - parts = [] - if entry_rank > 1: - parts.append("起点第 {0} 个".format(entry_rank)) - if exit_rank > 1: - parts.append("终点第 {0} 个".format(exit_rank)) - message += "\n接入候选:{0}。".format(",".join(parts)) - long_entry_warning = _long_network_entry_summary(report) - if long_entry_warning: - sample = long_entry_warning.get("sample", {}) - # 最终导线接入距离过长时,通常意味着设备附近缺少局部路径或主路径离端子太远。 - message += "\n接入距离提示:{0} 条导线起点/终点接入过长,示例导线 {1} {2},可能存在悬空或跨距过长。".format( - long_entry_warning.get("count", 0), - sample.get("wire", "未知导线"), - ",".join(sample.get("warning_parts", []) or []), - ) - route_source_sample = _route_source_sample_text(report) - if route_source_sample: - message += "\n{0}".format(route_source_sample) - quality_warning = _route_quality_warning_summary(report) - if quality_warning: - message += "\n路径质量提示:{0} 条导线使用布线面/辅助路径,可能没有完全优先进入线槽。".format( - quality_warning.get("count", 0) - ) - sample = quality_warning.get("sample", {}) - if isinstance(sample, dict) and sample.get("wire") and sample.get("labels"): - message += " 示例 {0} 使用{1}。".format( - sample.get("wire"), - "、".join(sample.get("labels", []) or []), - ) - errors = report.get("errors", []) or [] - if errors: - message += "\n首个错误:{0}".format(str(errors[0])) - error_sample = (report.get("error_samples") or [None])[0] - if error_sample: - message += "\n错误示例:导线 {0},{1}:{2}".format( - _wire_sample_text(error_sample), - _endpoint_pair_text(error_sample), - error_sample.get("error", ""), - ) - collision_sample = (report.get("collision_samples") or [None])[0] - if collision_sample: - obstacle_text = ( - collision_sample.get("obstacle_label") - or collision_sample.get("obstacle_name") - or "未知对象" - ) - wire_text = ( - collision_sample.get("wire_label") - or collision_sample.get("wire_uuid") - or "未知导线" - ) - if collision_sample.get("collision_kind") == "ClearanceWarning": - message += "\n碰撞示例:导线 {0} 进入 {1} 的安全间隙。".format( - wire_text, - obstacle_text, - ) - else: - message += "\n碰撞示例:导线 {0} 碰到 {1}。".format( - wire_text, - obstacle_text, - ) - auto_bound = report.get("auto_bound_terminals", 0) - auto_created = report.get("auto_created_terminals", 0) - if auto_bound or auto_created: - message += "\n已按导线任务绑定 3D 工程端子:更新 {0} 个,新建 {1} 个。".format( - auto_bound, - auto_created, - ) - if report.get("routed", 0) == 0 and report.get("skipped_missing_terminal", 0) > 0: - message += ( - "\n端子匹配失败:当前 3D 可布线端子 {0} 个,其中本地模板端子 {1} 个;" - "导线任务引用的 QET terminal_uuid 没有绑定到这些 3D 工程端子。" - ).format( - report.get("available_terminals", 0), - report.get("local_terminals", 0), - ) - if report.get("local_terminals", 0) > 0: - message += " 请先从 QET 重新导入/更新工程端子,使端子 UUID 不再是 local:...。" - sample = (report.get("missing_endpoint_samples") or [None])[0] - if sample: - message += "\n缺失示例:{0} -> {1}{2}".format( - _missing_endpoint_label(sample, "start"), - _missing_endpoint_label(sample, "end"), - _missing_endpoint_side_summary(sample), + left_matches = _find_route_bridge_sources_by_name_or_label(doc, name=left_label, label=left_label) + right_matches = _find_route_bridge_sources_by_name_or_label(doc, name=right_label, label=right_label) + if not left_matches or not right_matches: + missing_pairs.append(pair_text) + continue + new_bridges = RoutingNetwork.create_user_path_bridge_between_objects( + doc, + left_matches[0], + right_matches[0], + project_uuid=project_uuid, ) - return message + if new_bridges: + created.extend(new_bridges) + else: + duplicates += 1 + return { + "enabled": True, + "pairs": len(pair_counts), + "created_count": len(created), + "duplicates": duplicates, + "missing_pairs": missing_pairs, + "created_pair_labels": [ + getattr(bridge, "QetRouteBridgePairLabel", "") + for bridge in created + ], + "rerouted": False, + } -def _clear_routing_connection_batch_diagnostics(doc): - group = WiringObjects.ensure_diagnostic_group(doc, _project_uuid(doc)) - removed = 0 - for obj in list(getattr(group, "Group", []) or []): - if (getattr(obj, "QetDiagnosticKind", "") or "").strip() != "RoutingConnectionBatch": - continue - try: - group.removeObject(obj) - except Exception: + +def _result_route_points(result): + points = [] + if isinstance(result, dict): + for point in list(result.get("points", []) or []): try: - group.Group = [ - candidate - for candidate in list(getattr(group, "Group", []) or []) - if candidate is not obj - ] + points.append(_vector(point)) except Exception: - pass - try: - if doc.getObject(getattr(obj, "Name", "")) is not None: - doc.removeObject(obj.Name) - removed += 1 - except Exception: - pass - return removed + continue + if points: + return points + wire = result.get("wire") + for point in list(getattr(wire, "Points", []) or []): + try: + points.append(_vector(point)) + except Exception: + continue + return points -def _compact_route_sample(route): - route_track = route.get("route_track", {}) if isinstance(route, dict) else {} - if not isinstance(route_track, dict): - route_track = {} - sample = { - "wire_uuid": route.get("wire_uuid", ""), - "wire_label": route.get("wire_label", ""), - "start_terminal_uuid": route.get("start_terminal_uuid", ""), - "start_element_uuid": route.get("start_element_uuid", ""), - "start_terminal_display": route.get("start_terminal_display", ""), - "end_terminal_uuid": route.get("end_terminal_uuid", ""), - "end_element_uuid": route.get("end_element_uuid", ""), - "end_terminal_display": route.get("end_terminal_display", ""), - "endpoint_label": route.get("endpoint_label", ""), - "route_status": route.get("route_status", ""), - "length_mm": route.get("length_mm", 0.0), - "lane": route.get("lane", {}), - "algorithm": route.get("algorithm", ""), - "collision_count": route.get("collision_count", 0), - "carrier_kinds": _route_track_carrier_kinds(route_track), - "carrier_names": _route_track_carrier_names(route_track, limit=8), - "route_source_labels": _route_source_labels(route_track, limit=8), +def _route_points_equal(left_points, right_points, tolerance=0.001): + left = [_vector(point) for point in list(left_points or [])] + right = [_vector(point) for point in list(right_points or [])] + if len(left) != len(right): + return False + return all(_distance(left[index], right[index]) <= float(tolerance or 0.001) for index in range(len(left))) + + +def _set_route_carrier_capacity(carrier, capacity): + try: + setter = getattr(RoutingNetwork, "_set_route_carrier_capacity_value") + setter(carrier, capacity) + return + except Exception: + pass + try: + if "QetRouteCarrierCapacity" not in getattr(carrier, "PropertiesList", []): + carrier.addProperty( + "App::PropertyInteger", + "QetRouteCarrierCapacity", + "QET Routing", + "How many routed wires can reuse this carrier segment before detouring is preferred", + ) + carrier.QetRouteCarrierCapacity = max(int(capacity or 1), 1) + except Exception: + pass + + +def _find_existing_main_path_detour_user_path(doc, points): + target_points = _simplify_collinear_points(points) + if len(target_points) < 2: + return None + for carrier in RoutingNetwork.collect_route_carriers(doc): + if str(getattr(carrier, "QetRouteBridgeKind", "") or "").strip() != "MainPathDetourPath": + continue + if _route_points_equal(getattr(carrier, "Points", []) or [], target_points): + return carrier + return None + + +def _create_main_path_detour_user_path_from_retry(doc, retry_result, original_result, project_uuid=""): + points = _simplify_collinear_points(_result_route_points(retry_result)) + if len(points) < 2: + return None + lane_capacity = max(int(((retry_result or {}).get("lane", {}) or {}).get("index", 0) or 0) + 1, 1) + existing = _find_existing_main_path_detour_user_path(doc, points) + if existing is not None: + current_capacity = int(getattr(existing, "QetRouteCarrierCapacity", 1) or 1) + _set_route_carrier_capacity(existing, max(current_capacity + 1, lane_capacity)) + return existing + retry_quality = _route_quality_payload(retry_result.get("route_track", {})) + fallback_labels = list(retry_quality.get("fallback_carrier_labels", []) or []) + current_labels = _route_source_labels(original_result.get("route_track", {}), limit=4) if isinstance(original_result, dict) else [] + left_label = str(fallback_labels[0] if fallback_labels else "FallbackPath") + right_label = str(current_labels[0] if current_labels else "MainPath") + carrier = RoutingNetwork.create_route_carrier( + doc, + points, + label="QET Auto Main Path Detour {0} -> {1}".format(left_label, right_label), + project_uuid=project_uuid, + kind=RoutingNetwork.ROUTE_CARRIER_KIND_USER_PATH, + capacity=lane_capacity, + ) + # 这是把已验证的避障折线固化为正式 UserPath,不是放开 RoutingRange 兜底。 + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeKind", + "QET Routing", + "QET route bridge kind", + "MainPathDetourPath", + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgePairLabel", + "QET Routing", + "Human readable source pair for this generated detour path", + "{0} -> {1}".format(left_label, right_label), + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeLeftSourceLabel", + "QET Routing", + "Left/fallback source label for this generated detour path", + left_label, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRouteBridgeRightSourceLabel", + "QET Routing", + "Right/current source label for this generated detour path", + right_label, + ) + return carrier + + +def _main_path_detour_wire_uuids_from_report(report): + wire_uuids = [] + seen = set() + for route in list((report or {}).get("routes", []) or []): + if not isinstance(route, dict): + continue + issue_codes = {str(code or "").strip() for code in list(route.get("issue_codes", []) or [])} + if "main_path_detour_missing" not in issue_codes: + continue + wire_uuid = str(route.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 _wire_item_uuid(item): + if not isinstance(item, dict): + return "" + return str(_wire_item_value(item, "wire_id", "wire_uuid", "id") or "").strip() + + +def _payload_subset_for_wire_uuids(payload, wire_uuids): + if not isinstance(payload, dict): + return {} + wanted = {str(wire_uuid or "").strip() for wire_uuid in list(wire_uuids or []) if str(wire_uuid or "").strip()} + if not wanted: + return {} + subset = dict(payload) + subset["wires"] = [ + dict(item) + for item in list(payload.get("wires", []) or []) + if _wire_item_uuid(item) in wanted + ] + return subset if subset["wires"] else {} + + +def _recompute_route_report_after_route_replacement(doc, report): + routes = [route for route in list((report or {}).get("routes", []) or []) if isinstance(route, dict)] + route_status_counts = {} + wire_style_status_counts = {} + collision_samples = [] + total_length = 0.0 + collision_warnings = 0 + rejected_fallback = 0 + for route in routes: + status = str(route.get("route_status", "") or "").strip() or "Unknown" + route_status_counts[status] = route_status_counts.get(status, 0) + 1 + style_status = str(route.get("wire_style_status", "") or "").strip() + if style_status: + wire_style_status_counts[style_status] = wire_style_status_counts.get(style_status, 0) + 1 + if status == "CollisionWarning": + collision_warnings += 1 + if str(route.get("selective_collision_reroute_status", "") or "").strip() == "RejectedFallback": + rejected_fallback += 1 + total_length += float(route.get("length_mm", 0.0) or 0.0) + for collision in list(route.get("collisions", []) or [])[:3]: + if len(collision_samples) >= 8: + break + sample = dict(collision) if isinstance(collision, dict) else {} + if sample: + sample.setdefault("wire_uuid", route.get("wire_uuid", "")) + sample.setdefault("wire_label", route.get("wire_label", "")) + sample.setdefault("wire_object_label", route.get("wire_object_label", "")) + collision_samples.append(sample) + for status, count in ( + ("MissingTerminal", _safe_int(report.get("skipped_missing_terminal", 0))), + ("MissingRouteNetwork", _safe_int(report.get("skipped_missing_route_network", 0))), + ("Invalid", _safe_int(report.get("skipped_invalid", 0))), + ("Error", len(list(report.get("errors", []) or []))), + ): + if count > 0: + route_status_counts[status] = route_status_counts.get(status, 0) + count + + report["routes"] = routes + report["routed"] = len(routes) + report["collision_warnings"] = collision_warnings + report["total_length_mm"] = total_length + report["route_status_counts"] = route_status_counts + report["wire_style_status_counts"] = wire_style_status_counts + report["collision_samples"] = collision_samples + report["selective_collision_reroute_rejected_fallback"] = rejected_fallback + report["route_path_usage"] = _route_path_usage_summary(report) + report["top_collision_obstacles"] = _top_collision_obstacles(report) + report["issue_codes"] = _routing_connection_batch_issue_codes(report) + _attach_main_path_detour_report_summary(doc, report) + return report + + +def _merge_retry_routes_into_report(doc, report, retry_report): + if not isinstance(report, dict) or not isinstance(retry_report, dict): + return report + retry_routes = { + str(route.get("wire_uuid", "") or "").strip(): route + for route in list(retry_report.get("routes", []) or []) + if isinstance(route, dict) and str(route.get("wire_uuid", "") or "").strip() } - network = route.get("network", {}) - if isinstance(network, dict): - bridged_segments = network.get("bridged_segments", 0) - if "bridged_segments" in route_track: - bridged_segments = route_track.get("bridged_segments", 0) - sample["network"] = { - "carriers": network.get("carriers", 0), - "segments": network.get("segments", 0), - "bridged_segments": bridged_segments, - "blocked_segments": network.get("blocked_segments", 0), - "entry_distance": network.get("entry_distance", 0.0), - "exit_distance": network.get("exit_distance", 0.0), - } - return sample + if not retry_routes: + return report + merged_routes = [] + replaced = 0 + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + wire_uuid = str(route.get("wire_uuid", "") or "").strip() + if wire_uuid in retry_routes: + merged_routes.append(retry_routes[wire_uuid]) + replaced += 1 + 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 + return _recompute_route_report_after_route_replacement(doc, report) + + +def _raise_main_path_detour_capacities_from_report(doc, report): + if doc is None or not isinstance(report, dict): + return 0 + updated = 0 + for route in list(report.get("routes", []) or []): + if not isinstance(route, dict): + continue + lane_capacity = max(int(((route.get("lane", {}) or {}).get("index", 0) or 0)) + 1, 1) + route_track = route.get("route_track", {}) + if not isinstance(route_track, dict): + continue + for segment in list(route_track.get("segments", []) or []): + if not isinstance(segment, dict): + continue + carrier_payload = segment.get("carrier", {}) + if not isinstance(carrier_payload, dict): + continue + carrier_name = str(carrier_payload.get("name", "") or "").strip() + if not carrier_name: + continue + carrier = doc.getObject(carrier_name) + if carrier is None: + continue + if str(getattr(carrier, "QetRouteBridgeKind", "") or "").strip() != "MainPathDetourPath": + continue + current_capacity = int(getattr(carrier, "QetRouteCarrierCapacity", 1) or 1) + if current_capacity < lane_capacity: + _set_route_carrier_capacity(carrier, lane_capacity) + current_capacity = lane_capacity + updated += 1 + if int(carrier_payload.get("capacity", 1) or 1) < current_capacity: + carrier_payload["capacity"] = current_capacity + if updated > 0: + report["auto_main_path_detour_capacity_updates"] = updated + return updated def _compact_routing_connection_batch_report(report, sample_limit=8): @@ -2614,18 +8726,65 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): "route_network_segments", "route_network_nodes", "route_network_error", + "batch_network_entry_candidate_limit", + "missing_route_retry_candidate_limit", + "missing_route_retries", + "batch_avoid_obstacles", + "selective_collision_reroute", + "selective_collision_reroute_limit", + "selective_collision_reroute_allow_fallback", + "selective_collision_reroute_attempts", + "selective_collision_reroutes", + "selective_collision_reroute_no_improvement", + "selective_collision_reroute_rejected_fallback", + "selective_collision_reroute_errors", + "batch_obstacle_candidates", + "wire_style_database_path", + "wire_style_database_fallback_from", + "context_devices_loaded", + "context_device_count", + "context_devices_json_path", + "runtime_version", "hidden_route_carriers", + "routing_method", + "routing_path_network_updated", ) for key in scalar_keys: if key in report: payload[key] = report.get(key) + payload["issue_codes"] = _routing_connection_batch_issue_codes(report) + payload["issue_labels"] = [ + _routing_diagnostic_issue_label(code) + for code in payload["issue_codes"] + ] if isinstance(report.get("prepared_layout"), dict): payload["prepared_layout"] = report.get("prepared_layout") + if isinstance(report.get("routing_path_network_diagnostic"), dict): + payload["routing_path_network_diagnostic"] = report.get("routing_path_network_diagnostic") + if isinstance(report.get("auto_diagnostic_bridges"), dict): + payload["auto_diagnostic_bridges"] = dict(report.get("auto_diagnostic_bridges") or {}) + if isinstance(report.get("auto_main_path_detour_bridges"), dict): + payload["auto_main_path_detour_bridges"] = dict(report.get("auto_main_path_detour_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) + if carrier_kind_counts: + payload["route_network_carrier_kind_counts"] = carrier_kind_counts + payload["route_network_main_path_carriers"] = _route_network_main_path_carriers(report) + wire_style_status_counts = _wire_style_status_counts(report) + if wire_style_status_counts: + payload["wire_style_status_counts"] = wire_style_status_counts + missing_wire_style_samples = _wire_style_status_samples(report, status="Missing", limit=limit) + payload["missing_wire_style_samples"] = missing_wire_style_samples + payload["missing_wire_style_samples_count"] = len( + _wire_style_status_samples(report, status="Missing", limit=0) + ) missing_endpoint_uuids = list(report.get("missing_endpoint_uuids", []) or []) payload["missing_endpoint_uuid_count"] = len(missing_endpoint_uuids) payload["missing_endpoint_uuids"] = missing_endpoint_uuids[:50] + missing_terminal_summary = _batch_missing_terminal_summary(report) + if _safe_int(missing_terminal_summary.get("skipped_missing_terminal", 0)) > 0: + payload["missing_terminal_summary"] = missing_terminal_summary for key in ( "auto_terminal_binding_warnings", "missing_endpoint_samples", @@ -2637,10 +8796,33 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): values = list(report.get(key, []) or []) payload[key] = values[:limit] payload["{0}_count".format(key)] = len(values) + payload["collision_kind_counts"] = _collision_kind_counts(report) + collision_relation_counts = _collision_relation_counts(report) + if collision_relation_counts: + payload["collision_relation_counts"] = collision_relation_counts + collision_reroute_recommendation = _collision_reroute_recommendation(report) + if collision_reroute_recommendation: + payload["collision_reroute_recommendation"] = collision_reroute_recommendation + payload["top_collision_obstacles"] = _top_collision_obstacles(report, limit=limit) + collision_resolution_summary = _collision_resolution_summary(report, limit=limit) + if collision_resolution_summary: + payload["collision_resolution_summary"] = collision_resolution_summary + if isinstance(report.get("main_path_detour_missing_summary"), dict): + payload["main_path_detour_missing_summary"] = report.get("main_path_detour_missing_summary") + if isinstance(report.get("recommended_actions"), list): + payload["recommended_actions"] = list(report.get("recommended_actions") or []) routes = [route for route in list(report.get("routes", []) or []) if isinstance(route, dict)] payload["route_count"] = len(routes) - payload["route_samples"] = [_compact_route_sample(route) for route in routes[:limit]] + prioritized_routes = [ + route + for _index, route in sorted( + enumerate(routes), + key=lambda item: _route_sample_priority(item[1], item[0]), + ) + ] + 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) 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 @@ -2649,6 +8831,24 @@ def _compact_routing_connection_batch_report(report, sample_limit=8): _long_network_entry_warning_samples(report, limit=0) ) payload["route_entry_distance_warning_samples"] = entry_distance_warnings + candidate_obstacle_warnings = _route_candidate_obstacle_warning_samples(report, limit=limit) + payload["route_candidate_obstacle_warning_count"] = len( + _route_candidate_obstacle_warning_samples(report, limit=0) + ) + payload["route_candidate_obstacle_warning_samples"] = candidate_obstacle_warnings + candidate_boundary_warnings = _route_candidate_boundary_warning_samples(report, limit=limit) + payload["route_candidate_boundary_warning_count"] = len( + _route_candidate_boundary_warning_samples(report, limit=0) + ) + payload["route_candidate_boundary_warning_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 + capacity_pressure_warnings = _route_capacity_pressure_samples(report, limit=limit) + payload["route_capacity_pressure_warning_count"] = len( + _route_capacity_pressure_samples(report, limit=0) + ) + payload["route_capacity_pressure_warning_samples"] = capacity_pressure_warnings payload["diagnostic_payload"] = "compact-routing-connection-batch-v1" return payload @@ -2659,21 +8859,39 @@ def _write_routing_connection_batch_diagnostic(doc, report): project_uuid = _project_uuid(doc) group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) _clear_routing_connection_batch_diagnostics(doc) - if ( - report.get("total_wires", 0) <= 0 - and not report.get("routes") - and not report.get("errors") - and not report.get("missing_endpoint_uuids") - and report.get("collision_warnings", 0) <= 0 - ): - return None + compact_payload = _compact_routing_connection_batch_report(report) diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETRoutingConnectionDiagnostic")) diagnostic.Label = "QET Routing Connection Diagnostic" _set_string(diagnostic, "QetDiagnosticKind", "RoutingConnectionBatch", "QET diagnostic kind") + _set_string(diagnostic, "QetProjectUuid", project_uuid, "Project UUID") + _set_bool( + diagnostic, + "QetDiagnosticOk", + not bool(_routing_connection_batch_issue_codes(report)), + "QET diagnostic pass state", + ) + _set_string( + diagnostic, + "QetDiagnosticIssueCodes", + _diagnostic_issue_codes_text(compact_payload.get("issue_codes", [])), + "QET routing diagnostic issue codes", + ) + _set_string( + diagnostic, + "QetDiagnosticIssueLabels", + _diagnostic_issue_labels_text(compact_payload.get("issue_codes", [])), + "QET routing diagnostic issue labels", + ) + _set_string( + diagnostic, + "QetDiagnosticMessage", + format_eplan_connection_route_report(report), + "QET routing connection batch diagnostic message", + ) _set_string( diagnostic, "QetDiagnosticJson", - json.dumps(_compact_routing_connection_batch_report(report), ensure_ascii=False), + json.dumps(compact_payload, ensure_ascii=False), "QET routing connection batch diagnostic payload", ) group.addObject(diagnostic) @@ -2726,9 +8944,99 @@ def bind_wire_task_terminals_from_tasks(doc): return bind_wire_task_terminals_from_payload(doc, _wire_tasks_payload(doc)) +def _direct_task_routing_path_network_diagnostic(doc, opts): + try: + return _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), + ) + ) + except Exception as exc: + return { + "ok": False, + "issue_count": 1, + "issue_codes": ["routing_path_network_diagnostic_error"], + "issues": [ + { + "severity": "warning", + "code": "routing_path_network_diagnostic_error", + "count": 1, + } + ], + "summary": {}, + "error": str(exc), + } + + +def _direct_task_auto_diagnostic_bridge_report(doc, opts): + diagnostic = _direct_task_routing_path_network_diagnostic(doc, opts) + bridge_report = { + "enabled": bool(opts.get("auto_create_diagnostic_bridges", True)), + "suggestions": 0, + "created_count": 0, + "duplicates": 0, + "stale_suggestions": 0, + } + if not bridge_report["enabled"]: + return diagnostic, bridge_report + try: + result = RoutingNetwork.create_user_path_bridges_from_diagnostic_suggestions( + doc, + diagnostic, + project_uuid=_project_uuid(doc), + ) + created = list(result.get("created", []) or []) if isinstance(result, dict) else [] + bridge_report = { + "enabled": True, + "suggestions": int(result.get("suggestions", 0) or 0), + "created_count": len(created), + "duplicates": int(result.get("duplicates", 0) or 0), + "stale_suggestions": int(result.get("stale_suggestions", 0) or 0), + } + if created: + # 任务直连入口没有“更新路径网络”前置步骤;桥接创建后补一次诊断,让报告反映桥接后的网络状态。 + diagnostic = _direct_task_routing_path_network_diagnostic(doc, opts) + except Exception as exc: + bridge_report = { + "enabled": True, + "suggestions": 0, + "created_count": 0, + "duplicates": 0, + "stale_suggestions": 0, + "error": str(exc), + } + return diagnostic, bridge_report + + def route_eplan_connection_tasks(doc, options=None, prepared_layout=None): + opts = _merged_options(options) + routing_path_network_diagnostic = {} + auto_diagnostic_bridges = {} + if not bool(opts.get("__skip_task_auto_diagnostic_bridges", False)): + routing_path_network_diagnostic, auto_diagnostic_bridges = ( + _direct_task_auto_diagnostic_bridge_report(doc, opts) + ) payload = _wire_tasks_payload(doc) - return route_eplan_connections_from_payload(doc, payload, options=options, prepared_layout=prepared_layout) + report = route_eplan_connections_from_payload(doc, payload, options=opts, prepared_layout=prepared_layout) + report_changed = False + if ( + isinstance(routing_path_network_diagnostic, dict) + and routing_path_network_diagnostic + and not _has_routing_path_network_diagnostic(report) + ): + report["routing_path_network_diagnostic"] = routing_path_network_diagnostic + report_changed = True + if isinstance(auto_diagnostic_bridges, dict) and auto_diagnostic_bridges: + report["auto_diagnostic_bridges"] = auto_diagnostic_bridges + report_changed = True + if report_changed: + report["issue_codes"] = _routing_connection_batch_issue_codes(report) + _write_routing_connection_batch_diagnostic(doc, report) + return report def prepare_eplan_layout_space(doc, project_uuid=""): @@ -2789,6 +9097,7 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None): project_uuid=target_project_uuid, 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), ) diagnostic = result.get("diagnostic", {}) if isinstance(result, dict) else {} @@ -2797,6 +9106,7 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None): "diagnostic_object": result.get("diagnostic_object") if isinstance(result, dict) else None, "ok": bool(diagnostic.get("ok", False)) if isinstance(diagnostic, dict) else False, "issue_count": len(diagnostic.get("issues", []) or []) if isinstance(diagnostic, dict) else 0, + "issue_codes": list(diagnostic.get("issue_codes", []) or []) if isinstance(diagnostic, dict) else [], } @@ -2804,7 +9114,7 @@ def _compact_routing_path_network_diagnostic(diagnostic): if not isinstance(diagnostic, dict): return {} issues = _dict_items(diagnostic.get("issues", []) or []) - return { + payload = { "ok": bool(diagnostic.get("ok", False)), "issue_count": len(issues), "issue_codes": [str(issue.get("code", "") or "") for issue in issues if issue.get("code", "")], @@ -2818,6 +9128,68 @@ def _compact_routing_path_network_diagnostic(diagnostic): ], "summary": diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {}, } + outside_carriers = _dict_items(diagnostic.get("route_carriers_outside_boundary", []) or []) + if outside_carriers: + payload["route_carriers_outside_boundary"] = [ + { + "carrier": item.get("carrier", {}) if isinstance(item.get("carrier", {}), dict) else {}, + "outside_point_count": _safe_int(item.get("outside_point_count", 0)), + "outside_points": list(item.get("outside_points", []) or [])[:3], + } + for item in outside_carriers[:5] + ] + outside_terminals = _dict_items(diagnostic.get("terminals_outside_boundary", []) or []) + if outside_terminals: + payload["terminals_outside_boundary"] = [ + { + "name": item.get("name", ""), + "label": item.get("label", ""), + "terminal_uuid": item.get("terminal_uuid", ""), + "instance_id": item.get("instance_id", ""), + "outside_point_count": _safe_int(item.get("outside_point_count", 0)), + "outside_points": list(item.get("outside_points", []) or [])[:3], + } + for item in outside_terminals[:5] + ] + long_accesses = _dict_items(diagnostic.get("long_terminal_accesses", []) or []) + if long_accesses: + payload["long_terminal_accesses"] = [ + { + "name": item.get("name", ""), + "label": item.get("label", ""), + "terminal_uuid": item.get("terminal_uuid", ""), + "instance_id": item.get("instance_id", ""), + "terminal_origin": item.get("terminal_origin", {}), + "parent_device_name": item.get("parent_device_name", ""), + "parent_device_label": item.get("parent_device_label", ""), + "parent_device_instance_id": item.get("parent_device_instance_id", ""), + "parent_device_element_uuid": item.get("parent_device_element_uuid", ""), + "access_carrier": item.get("access_carrier", ""), + "terminal_access_length_mm": item.get("terminal_access_length_mm", 0.0), + "terminal_access_warning_distance_mm": item.get("terminal_access_warning_distance_mm", 0.0), + "terminal_access_max_distance_mm": item.get("terminal_access_max_distance_mm", 0.0), + "terminal_access_dominant_axis": item.get("terminal_access_dominant_axis", ""), + "terminal_access_axis_lengths_mm": item.get("terminal_access_axis_lengths_mm", {}), + "terminal_access_points": list(item.get("terminal_access_points", []) or [])[:6], + } + for item in long_accesses[:5] + ] + wire_duct_components = _dict_items(diagnostic.get("wire_ducts_without_terminal_access", []) or []) + if wire_duct_components: + payload["wire_ducts_without_terminal_access"] = [ + { + "index": item.get("index"), + "nodes": _safe_int(item.get("nodes", 0)), + "segments": _safe_int(item.get("segments", 0)), + "carrier_kinds": item.get("carrier_kinds", {}) if isinstance(item.get("carrier_kinds", {}), dict) else {}, + "carrier_names": list(item.get("carrier_names", []) or [])[:8], + "bridge_suggestion": item.get("bridge_suggestion", {}) + if isinstance(item.get("bridge_suggestion", {}), dict) + else {}, + } + for item in wire_duct_components[:5] + ] + return payload _PATH_NETWORK_ISSUE_LABELS = { @@ -2825,9 +9197,12 @@ _PATH_NETWORK_ISSUE_LABELS = { "invalid_route_carriers": "路径对象几何无效", "routing_range_only_network": "仅使用布线面兜底", "invalid_terminal_local_routes": "端子局部路径无效", + "route_carriers_outside_boundary": "路径越出柜内边界", + "terminals_outside_boundary": "端子越出柜内边界", "long_terminal_accesses": "端子接入过长", "unconnected_terminals": "端子未接入", "wire_duct_endpoint_breaks": "线槽端点疑似断开", + "wire_ducts_without_terminal_access": "线槽未接入端子主网络", "isolated_network_components": "存在孤立路径网络", } @@ -2875,6 +9250,13 @@ def _dict_items(value): return [item for item in value if isinstance(item, dict)] +def _safe_int(value, fallback=0): + try: + return int(value or 0) + except Exception: + return int(fallback or 0) + + def format_routing_path_network_report(diagnostic): """Return an actionable Chinese summary for routing path network diagnostics.""" if not isinstance(diagnostic, dict): @@ -2917,6 +9299,25 @@ def format_routing_path_network_report(diagnostic): _format_point_text(sample.get("point")), ) + wire_duct_components = _dict_items(diagnostic.get("wire_ducts_without_terminal_access", []) or []) + if wire_duct_components: + sample = wire_duct_components[0] + carriers = sample.get("carrier_names") or [] + carrier_text = "、".join([str(item) for item in carriers[:3]]) if carriers else "未知线槽" + suggestion = sample.get("bridge_suggestion", {}) + if isinstance(suggestion, dict) and suggestion: + target = suggestion.get("to_carrier", {}) if isinstance(suggestion.get("to_carrier", {}), dict) else {} + target_text = target.get("label") or target.get("name") or "主网络" + message += "\n线槽未接入端子主网络:{0},建议桥接到 {1},距离 {2}。请选中这两段路径后点击“选中两路径生成桥接”。".format( + carrier_text, + target_text, + _format_distance_mm(suggestion.get("distance_mm")), + ) + else: + message += "\n线槽未接入端子主网络:{0}。请用 UserPath/线槽开口/桥接路径把线槽接到端子接入所在的主网络。".format( + carrier_text + ) + invalid_carriers = _dict_items(diagnostic.get("invalid_route_carriers", []) or []) if invalid_carriers: sample = invalid_carriers[0] @@ -2926,6 +9327,24 @@ def format_routing_path_network_report(diagnostic): carrier_text ) + outside_carriers = _dict_items(diagnostic.get("route_carriers_outside_boundary", []) or []) + if outside_carriers: + sample = outside_carriers[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + carrier_text = carrier.get("label") or carrier.get("name") or "未知路径对象" + message += "\n路径越出柜内边界:{0},越界点 {1} 个。请把该线槽/UserPath 调整到柜内,或重新标记正确的柜内边界。".format( + carrier_text, + _safe_int(sample.get("outside_point_count", 0)), + ) + + outside_terminals = _dict_items(diagnostic.get("terminals_outside_boundary", []) or []) + if outside_terminals: + sample = outside_terminals[0] + message += "\n端子越出柜内边界:{0},越界点 {1} 个。请确认设备已经装配到柜内,或重新标记正确的柜内边界。".format( + _diagnostic_terminal_text(sample), + _safe_int(sample.get("outside_point_count", 0)), + ) + long_accesses = _dict_items(diagnostic.get("long_terminal_accesses", []) or []) if long_accesses: sample = long_accesses[0] @@ -2955,7 +9374,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 invalid_carriers 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_local_routes or routing_range_only or isolated): first_issue = issues[0] message += "\n首个问题:{0} ({1})。".format( first_issue.get("code", "unknown"), @@ -2996,12 +9415,20 @@ def route_eplan_connections( selection_ex=selection_ex, ) routing_path_network_diagnostic = {} + auto_diagnostic_bridges = { + "enabled": bool(opts.get("auto_create_diagnostic_bridges", True)), + "suggestions": 0, + "created_count": 0, + "duplicates": 0, + "stale_suggestions": 0, + } 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), ) ) @@ -3021,9 +9448,53 @@ def route_eplan_connections( "error": str(exc), } + if bool(opts.get("auto_create_diagnostic_bridges", True)): + try: + bridge_report = RoutingNetwork.create_user_path_bridges_from_diagnostic_suggestions( + doc, + routing_path_network_diagnostic, + project_uuid=(project_uuid or _project_uuid(doc)), + ) + created = list(bridge_report.get("created", []) or []) if isinstance(bridge_report, dict) else [] + auto_diagnostic_bridges = { + "enabled": True, + "suggestions": int(bridge_report.get("suggestions", 0) or 0), + "created_count": len(created), + "duplicates": int(bridge_report.get("duplicates", 0) or 0), + "stale_suggestions": int(bridge_report.get("stale_suggestions", 0) or 0), + } + if created: + if update_network: + prepared_network = update_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=opts, + selection_ex=selection_ex, + ) + 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), + ) + ) + except Exception as exc: + auto_diagnostic_bridges = { + "enabled": True, + "suggestions": 0, + "created_count": 0, + "duplicates": 0, + "stale_suggestions": 0, + "error": str(exc), + } + target_payload = payload if target_payload is None: - target_payload = getattr(App, "_qet_exchange_payload", None) + candidate_payload = getattr(App, "_qet_exchange_payload", None) + if _payload_matches_document_project(doc, candidate_payload): + target_payload = candidate_payload if isinstance(target_payload, dict) and target_payload.get("wires"): report = route_eplan_connections_from_payload( @@ -3033,21 +9504,81 @@ def route_eplan_connections( prepared_layout=prepared_network, ) else: + task_route_options = dict(opts) + task_route_options["__skip_task_auto_diagnostic_bridges"] = True report = route_eplan_connection_tasks( doc, - options=opts, + options=task_route_options, prepared_layout=prepared_network, ) + auto_main_path_detour_bridges = { + "enabled": bool(opts.get("auto_create_main_path_detour_bridges", True)), + "pairs": 0, + "created_count": 0, + "duplicates": 0, + "missing_pairs": [], + "created_pair_labels": [], + "rerouted": False, + } + if bool(opts.get("auto_create_main_path_detour_bridges", True)): + try: + auto_main_path_detour_bridges = _create_main_path_detour_bridges_from_report( + doc, + report, + project_uuid=(project_uuid or _project_uuid(doc)), + ) + if int(auto_main_path_detour_bridges.get("created_count", 0) or 0) > 0: + retry_wire_uuids = _main_path_detour_wire_uuids_from_report(report) + if update_network: + prepared_network = update_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=opts, + selection_ex=selection_ex, + ) + retry_payload = _payload_subset_for_wire_uuids(target_payload, retry_wire_uuids) + 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) + auto_main_path_detour_bridges["retry_wires"] = len(retry_payload.get("wires", []) or []) + auto_main_path_detour_bridges["retry_replaced_routes"] = int( + report.get("main_path_detour_retry_replaced_routes", 0) or 0 + ) + auto_main_path_detour_bridges["rerouted"] = True + else: + auto_main_path_detour_bridges["retry_wires"] = 0 + auto_main_path_detour_bridges["retry_replaced_routes"] = 0 + auto_main_path_detour_bridges["rerouted"] = False + except Exception as exc: + auto_main_path_detour_bridges = { + "enabled": True, + "pairs": 0, + "created_count": 0, + "duplicates": 0, + "missing_pairs": [], + "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 if isinstance(prepared_network, dict): report["routing_path_network"] = prepared_network if opts.get("hide_route_carriers_after_route", True): report["hidden_route_carriers"] = RoutingNetwork.set_route_carriers_visibility(doc, False) else: report["hidden_route_carriers"] = 0 + _write_routing_connection_batch_diagnostic(doc, report) return report def wire_task_count(doc): diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 3ce310b..566f062 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -5,6 +5,7 @@ # 2. "生成布线路径网络" - generate the full routing path network # 3. "生成布线连接" - update the network and route all QET wire tasks +import json import FreeCAD as App try: @@ -118,6 +119,13 @@ class AutoRoutingController: max_distance = RoutingNetwork.DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE self.options["terminal_access_max_distance"] = max(max_distance, 0.0) + def set_terminal_access_warning_distance(self, value): + try: + warning_distance = float(value) + except Exception: + warning_distance = AutoRouting.DEFAULT_OPTIONS["terminal_access_warning_distance"] + self.options["terminal_access_warning_distance"] = max(warning_distance, 0.0) + def set_terminal_exit_length(self, value): try: exit_length = float(value) @@ -125,6 +133,20 @@ class AutoRoutingController: exit_length = AutoRouting.DEFAULT_OPTIONS["terminal_exit_length"] self.options["terminal_exit_length"] = max(exit_length, 0.0) + def set_obstacle_clearance(self, value): + try: + clearance = float(value) + except Exception: + clearance = AutoRouting.DEFAULT_OPTIONS["obstacle_clearance"] + self.options["obstacle_clearance"] = max(clearance, 0.0) + + def set_segment_reuse_penalty(self, value): + try: + penalty = float(value) + except Exception: + penalty = AutoRouting.DEFAULT_OPTIONS["segment_reuse_penalty"] + self.options["segment_reuse_penalty"] = max(penalty, 0.0) + def set_lane_spacing(self, value): try: lane_spacing = float(value) @@ -145,6 +167,20 @@ class AutoRoutingController: lane_axis = AutoRouting.DEFAULT_OPTIONS["lane_axis"] self.options["lane_axis"] = lane_axis + def set_selected_route_capacity(self, value): + try: + capacity = int(float(value)) + except Exception: + capacity = 1 + self.options["selected_route_capacity"] = max(capacity, 1) + + def set_preflight_routeability_sample_limit(self, value): + try: + sample_limit = int(value) + except Exception: + sample_limit = int(AutoRouting.DEFAULT_OPTIONS["preflight_routeability_sample_limit"]) + self.options["preflight_routeability_sample_limit"] = max(sample_limit, 0) + def summary(self): doc = _active_document() terminal_count = len(AutoRouting.index_terminals(doc)) @@ -153,6 +189,13 @@ class AutoRoutingController: payload_wire_count = 0 if isinstance(payload, dict) and isinstance(payload.get("wires"), list): payload_wire_count = len(payload.get("wires") or []) + boundary_count = len(AutoRouting.collect_routing_boundaries(doc)) + route_constraints = RoutingNetwork.collect_route_constraint_options(doc) + required_constraint_count = len(route_constraints.get("required_route_carrier_names", []) or []) + forbidden_constraint_count = len(route_constraints.get("forbidden_route_carrier_names", []) or []) + source_constraint_counts = RoutingNetwork.collect_route_constraint_source_counts(doc) + required_source_constraint_count = int(source_constraint_counts.get("required", 0) or 0) + forbidden_source_constraint_count = int(source_constraint_counts.get("forbidden", 0) or 0) network = RoutingNetwork.network_summary( doc, adjoining_duct_tolerance=float( @@ -173,7 +216,58 @@ class AutoRoutingController: bridge_text = "" if int(network.get("bridged_segments", 0) or 0) > 0: bridge_text = ";桥接:{0}".format(network.get("bridged_segments", 0)) - return "端子:{0};导线任务:{1};QET导线:{2};路由网络:{3} 条 carrier / {4} 段 / {5} 节点{6}{7}".format( + boundary_text = "" + if boundary_count > 0: + boundary_text = ";柜内边界:{0}".format(boundary_count) + obstacle_mode_text = "" + try: + obstacle_modes = AutoRouting.routing_obstacle_mode_summary(doc) + except Exception: + obstacle_modes = {} + if isinstance(obstacle_modes, dict): + pass_through = obstacle_modes.get("PassThrough", {}) + if isinstance(pass_through, dict): + pass_through_count = int(pass_through.get("count", 0) or 0) + if pass_through_count > 0: + obstacle_mode_text = ";忽略碰撞:{0}".format(pass_through_count) + constraint_text = "" + if required_constraint_count > 0 or forbidden_constraint_count > 0: + constraint_text = ";路径约束:必经 {0},禁经 {1}".format( + required_constraint_count, + forbidden_constraint_count, + ) + source_constraint_text = "" + if required_source_constraint_count > 0 or forbidden_source_constraint_count > 0: + source_constraint_text = ";源路径约束:必经 {0},禁经 {1}".format( + required_source_constraint_count, + forbidden_source_constraint_count, + ) + style_database_text = "" + exchange_summary = getattr(App, "_qet_exchange_summary", None) + style_database_path = "" + doc_project_uuid = str( + getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "") or "" + ).strip() + if isinstance(payload, dict): + payload_project_uuid = str(payload.get("project_uuid", "") or "").strip() + payload_matches_doc = not ( + doc_project_uuid and payload_project_uuid and doc_project_uuid != payload_project_uuid + ) + else: + payload_matches_doc = False + if payload_matches_doc: + style_database_path = str(payload.get("wire_style_database_path", "") or "").strip() + if not style_database_path and isinstance(exchange_summary, dict): + summary_project_uuid = str(exchange_summary.get("project_uuid", "") or "").strip() + summary_matches_doc = not ( + doc_project_uuid and summary_project_uuid and doc_project_uuid != summary_project_uuid + ) + if summary_matches_doc: + style_database_path = str(exchange_summary.get("wire_style_database_path", "") or "").strip() + if style_database_path: + style_database_text = ";导线样式库:{0}".format(style_database_path) + version_text = ";版本:{0}".format(AutoRouting.AUTO_ROUTING_RUNTIME_VERSION) + return "端子:{0};导线任务:{1};QET导线:{2};路由网络:{3} 条 carrier / {4} 段 / {5} 节点{6}{7}{8}{9}{10}{11}{12}{13}".format( terminal_count, task_count, payload_wire_count, @@ -182,6 +276,12 @@ class AutoRoutingController: network.get("nodes", 0), kind_text, bridge_text, + boundary_text, + obstacle_mode_text, + constraint_text, + source_constraint_text, + style_database_text, + version_text, ) def generate_routing_paths(self): @@ -209,6 +309,1507 @@ class AutoRoutingController: ) return self.last_report + def check_routing_readiness(self): + doc = _active_document() + payload = getattr(App, "_qet_exchange_payload", None) + self.last_report = AutoRouting.preflight_eplan_connections( + doc, + payload=payload if isinstance(payload, dict) else None, + options=self.routing_options(), + ) + AutoRouting.write_routing_preflight_diagnostic(doc, self.last_report) + return self.last_report + + def collect_routing_diagnostic_summary(self): + doc = _active_document() + self.last_report = AutoRouting.collect_routing_diagnostic_summary(doc) + AutoRouting.write_routing_diagnostic_summary(doc, self.last_report) + return self.last_report + + def select_top_collision_obstacles(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + obstacles = list(summary.get("batch_top_collision_obstacles", []) or []) + selected = [] + missing_names = [] + + try: + Gui.Selection.clearSelection() + except Exception: + pass + for item in obstacles: + if not isinstance(item, dict): + continue + obj = self._find_object_by_name_or_label( + doc, + item.get("name", ""), + item.get("label", ""), + ) + if obj is None: + missing = str(item.get("name", "") or item.get("label", "") or "").strip() + if missing: + missing_names.append(missing) + continue + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(obj, "Name", "")) + except Exception: + continue + selected.append(obj) + self.last_report = { + "selected_collision_obstacles": len(selected), + "selected_collision_obstacle_names": [getattr(obj, "Name", "") for obj in selected], + "missing_collision_obstacle_names": missing_names, + } + return self.last_report + + @staticmethod + def _is_device_or_layout_collision_candidate(item): + code = str(item.get("resolution_hint_code", "") or "").strip() + if code == "review_device_or_layout_collision": + return True + if code: + return False + hint = getattr(AutoRouting, "_collision_obstacle_resolution_hint", lambda _item: {})(item) + return ( + isinstance(hint, dict) + and str(hint.get("code", "") or "").strip() == "review_device_or_layout_collision" + ) + + def select_device_or_layout_collision_obstacles(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + obstacles = list(summary.get("batch_top_collision_obstacles", []) or []) + selected = [] + missing_names = [] + + try: + Gui.Selection.clearSelection() + except Exception: + pass + for item in obstacles: + if not isinstance(item, dict) or not self._is_device_or_layout_collision_candidate(item): + continue + obj = self._find_object_by_name_or_label( + doc, + item.get("name", ""), + item.get("label", ""), + ) + if obj is None: + missing = str(item.get("name", "") or item.get("label", "") or "").strip() + if missing: + missing_names.append(missing) + continue + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(obj, "Name", "")) + except Exception: + continue + selected.append(obj) + self.last_report = { + "selected_device_or_layout_collision_obstacles": len(selected), + "selected_device_or_layout_collision_obstacle_names": [getattr(obj, "Name", "") for obj in selected], + "missing_device_or_layout_collision_obstacle_names": missing_names, + } + return self.last_report + + def select_top_collision_parent_assemblies(self): + result = self._select_collision_parent_assemblies() + self.last_report = { + "selected_collision_parent_assemblies": result["selected_parent_assemblies"], + "selected_collision_parent_assembly_names": result["selected_parent_assembly_names"], + "missing_collision_parent_assembly_refs": result["missing_parent_assembly_refs"], + } + return self.last_report + + @staticmethod + def _is_structural_collision_candidate(item): + code = str(item.get("resolution_hint_code", "") or "").strip() + if code == "review_pass_through_structural_obstacle": + return True + if code: + return False + hint = getattr(AutoRouting, "_collision_obstacle_resolution_hint", lambda _item: {})(item) + return ( + isinstance(hint, dict) + and str(hint.get("code", "") or "").strip() == "review_pass_through_structural_obstacle" + ) + + def select_structural_collision_parent_assemblies(self): + result = self._select_collision_parent_assemblies( + filter_fn=self._is_structural_collision_candidate, + nearest_structural_parent=True, + ) + self.last_report = { + "selected_structural_collision_parent_assemblies": result["selected_parent_assemblies"], + "selected_structural_collision_parent_assembly_names": result["selected_parent_assembly_names"], + "missing_structural_collision_parent_assembly_refs": result["missing_parent_assembly_refs"], + } + return self.last_report + + def mark_structural_collision_parent_assemblies_pass_through(self): + result = self._select_collision_parent_assemblies( + filter_fn=self._is_structural_collision_candidate, + nearest_structural_parent=True, + select=False, + ) + marked = [] + marked_names = [] + for obj in list(result.get("parent_assembly_objects", []) or []): + if obj is None: + continue + RoutingNetwork.set_routing_obstacle_mode(obj, "PassThrough") + marked.append(obj) + marked_names.append(getattr(obj, "Name", "")) + try: + _active_document().recompute() + except Exception: + pass + self.last_report = { + "marked_structural_collision_parent_assemblies": len(marked), + "marked_structural_collision_parent_assembly_names": marked_names, + "missing_structural_collision_parent_assembly_refs": result["missing_parent_assembly_refs"], + "obstacle_mode": "PassThrough", + } + return self.last_report + + @staticmethod + def _is_broad_collision_parent_ref(name, label): + text = " ".join([str(name or ""), str(label or "")]).strip().lower() + if not text: + return False + broad_refs = ( + "qet exchange devices", + "qetexchangedevices", + ) + return any(ref in text for ref in broad_refs) + + @staticmethod + def _is_structural_collision_parent_ref(name, label): + if AutoRoutingController._is_broad_collision_parent_ref(name, label): + return False + text = " ".join([str(name or ""), str(label or "")]).strip().lower() + if not text: + return False + keywords = getattr( + AutoRouting, + "_STRUCTURAL_COLLISION_KEYWORDS", + ("cabinet", "door", "cover", "panel", "bracket", "support", "shell", "柜", "门", "盖", "板", "支架", "壳"), + ) + return any(str(keyword or "").lower() in text for keyword in keywords) + + @classmethod + def _nearest_structural_parent_refs(cls, parent_names, parent_labels): + max_count = max(len(parent_names), len(parent_labels)) + fallback = None + for index in range(max_count): + name = parent_names[index] if index < len(parent_names) else "" + label = parent_labels[index] if index < len(parent_labels) else "" + if cls._is_broad_collision_parent_ref(name, label): + continue + if fallback is None: + fallback = (name, label) + if cls._is_structural_collision_parent_ref(name, label): + return [(name, label)] + return [fallback] if fallback is not None else [] + + def _select_collision_parent_assemblies(self, filter_fn=None, select=True, nearest_structural_parent=False): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + obstacles = list(summary.get("batch_top_collision_obstacles", []) or []) + selected = [] + selected_names = set() + missing_refs = [] + seen_refs = set() + + if select: + try: + Gui.Selection.clearSelection() + except Exception: + pass + for item in obstacles: + if not isinstance(item, dict): + continue + if callable(filter_fn) and not filter_fn(item): + continue + parent_names = list(item.get("parent_names", []) or []) + parent_labels = list(item.get("parent_labels", []) or []) + parent_refs = ( + self._nearest_structural_parent_refs(parent_names, parent_labels) + if nearest_structural_parent + else [ + ( + parent_names[index] if index < len(parent_names) else "", + parent_labels[index] if index < len(parent_labels) else "", + ) + for index in range(max(len(parent_names), len(parent_labels))) + ] + ) + for name, label in parent_refs: + ref = str(name or label or "").strip() + if not ref or ref in seen_refs: + continue + seen_refs.add(ref) + obj = self._find_object_by_name_or_label(doc, name, label) + if obj is None: + missing_refs.append(ref) + continue + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + continue + if select: + 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) + return { + "selected_parent_assemblies": len(selected), + "selected_parent_assembly_names": [getattr(obj, "Name", "") for obj in selected], + "missing_collision_parent_assembly_refs": missing_refs, + "missing_parent_assembly_refs": missing_refs, + "parent_assembly_objects": selected, + } + + @staticmethod + def _find_object_by_name_or_label(doc, name, label): + name = str(name or "").strip() + label = str(label or "").strip() + if name: + obj = doc.getObject(name) + if obj is not None: + return obj + if label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == label: + return candidate + return None + + @staticmethod + def _route_track_refs(route_track): + carrier_refs = [] + source_refs = [] + if not isinstance(route_track, dict): + return carrier_refs, source_refs + for name in list(route_track.get("carrier_names", []) or []): + carrier_refs.append({"name": name, "label": ""}) + for segment in list(route_track.get("segments", []) or []): + if not isinstance(segment, dict): + continue + carrier_payload = segment.get("carrier", {}) if isinstance(segment.get("carrier", {}), dict) else {} + if not carrier_payload: + continue + carrier_refs.append( + { + "name": carrier_payload.get("name", ""), + "label": carrier_payload.get("label", ""), + } + ) + source_name = str(carrier_payload.get("source_name", "") or "").strip() + source_label = str(carrier_payload.get("source_label", "") or "").strip() + if source_name or source_label: + source_refs.append({"name": source_name, "label": source_label}) + return carrier_refs, source_refs + + def _select_route_refs(self, doc, route_tracks, result_prefix): + selected = [] + selected_names = set() + selected_carriers = [] + selected_sources = [] + missing_refs = [] + + try: + Gui.Selection.clearSelection() + except Exception: + pass + + def add_object(obj, bucket): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + return False + 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) + + for route_track in list(route_tracks or []): + carrier_refs, source_refs = self._route_track_refs(route_track) + for ref in carrier_refs: + name = str(ref.get("name", "") or "").strip() + label = str(ref.get("label", "") or "").strip() + if not name and not label: + continue + carrier = self._find_object_by_name_or_label(doc, name, label) + if carrier is None: + remember_missing(name or label) + continue + add_object(carrier, selected_carriers) + for ref in source_refs: + name = str(ref.get("name", "") or "").strip() + label = str(ref.get("label", "") or "").strip() + source = self._find_object_by_name_or_label(doc, name, label) + if source is None: + remember_missing(name or label) + continue + add_object(source, selected_sources) + + self.last_report = { + "{0}_objects".format(result_prefix): len(selected), + "{0}_carriers".format(result_prefix): len(selected_carriers), + "{0}_carrier_names".format(result_prefix): [getattr(obj, "Name", "") for obj in selected_carriers], + "{0}_sources".format(result_prefix): len(selected_sources), + "{0}_source_names".format(result_prefix): [getattr(obj, "Name", "") for obj in selected_sources], + "missing_{0}_refs".format(result_prefix): missing_refs, + } + return self.last_report + + def select_collision_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("collision_samples", []) or []): + add_sample(sample) + for sample in list(batch_payload.get("route_samples", []) or []): + issue_codes = [str(code or "") for code in list(sample.get("issue_codes", []) or [])] + if "collision_warnings" 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) + collision_issue_codes = { + "collision_warnings", + "third_party_device_collisions", + "endpoint_device_collisions", + } + 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(collision_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) + self.last_report = { + "selected_collision_wires": len(selected), + "selected_collision_wire_names": [getattr(obj, "Name", "") for obj in selected], + "missing_collision_wire_refs": missing_refs, + } + return self.last_report + + def select_issue_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() + + for item in list(batch_payload.get("route_samples", []) or []): + if not isinstance(item, dict): + continue + issue_codes = [ + str(code or "").strip() + for code in list(item.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if not issue_codes: + continue + 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: + continue + seen_refs.add(key) + samples.append({"wire_uuid": wire_uuid, "wire_label": wire_label, "ref": key}) + + 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) + 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_text = str(getattr(candidate, "QetRouteIssueCodes", "") or "").strip() + if not issue_codes_text: + 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) + self.last_report = { + "selected_issue_wires": len(selected), + "selected_issue_wire_names": [getattr(obj, "Name", "") for obj in selected], + "missing_issue_wire_refs": missing_refs, + } + return self.last_report + + def select_main_path_detour_missing_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() + + for item in list(batch_payload.get("route_samples", []) or []): + if not isinstance(item, dict): + continue + issue_codes = [ + str(code or "").strip() + for code in list(item.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if "main_path_detour_missing" not in issue_codes: + continue + 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: + continue + seen_refs.add(key) + samples.append({"wire_uuid": wire_uuid, "wire_label": wire_label, "ref": key}) + + 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) + + # 真实工程里有时 route_samples 只保留样例,已生成导线属性才是完整问题清单。 + 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 "main_path_detour_missing" not in 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) + self.last_report = { + "selected_main_path_detour_missing_wires": len(selected), + "selected_main_path_detour_missing_wire_names": [getattr(obj, "Name", "") for obj in selected], + "missing_main_path_detour_missing_wire_refs": missing_refs, + } + return self.last_report + + def select_issue_route_sources(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("route_samples", []) or []) + route_tracks = [] + + for sample in samples: + if not isinstance(sample, dict): + continue + issue_codes = [ + str(code or "").strip() + for code in list(sample.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if not issue_codes: + continue + route_track = sample.get("route_track", {}) if isinstance(sample.get("route_track", {}), dict) else {} + if list(sample.get("carrier_names", []) or []): + route_track = dict(route_track) + route_track["carrier_names"] = list(sample.get("carrier_names", []) or []) + list( + route_track.get("carrier_names", []) or [] + ) + route_tracks.append(route_track) + + report = self._select_route_refs(doc, route_tracks, "selected_issue_route") + report["missing_issue_route_refs"] = report.pop("missing_selected_issue_route_refs", []) + return report + + def select_main_path_detour_missing_route_sources(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 {} + ) + route_tracks = [] + for sample in list(batch_payload.get("route_samples", []) or []): + if not isinstance(sample, dict): + continue + issue_codes = [ + str(code or "").strip() + for code in list(sample.get("issue_codes", []) or []) + if str(code or "").strip() + ] + if "main_path_detour_missing" not in issue_codes: + continue + route_track = sample.get("route_track", {}) if isinstance(sample.get("route_track", {}), dict) else {} + if list(sample.get("carrier_names", []) or []): + route_track = dict(route_track) + route_track["carrier_names"] = list(sample.get("carrier_names", []) or []) + list( + route_track.get("carrier_names", []) or [] + ) + route_tracks.append(route_track) + seen_wire_tracks = set() + for candidate in list(getattr(doc, "Objects", []) or []): + if (getattr(candidate, "RouteType", "") or "").strip() != "RoutedConnection": + continue + issue_codes = { + code.strip() + for code in str(getattr(candidate, "QetRouteIssueCodes", "") or "").split(",") + if code.strip() + } + if "main_path_detour_missing" not in issue_codes: + continue + route_track_text = str(getattr(candidate, "QetRouteTrackJson", "") or "").strip() + if not route_track_text: + continue + wire_name = str(getattr(candidate, "Name", "") or "").strip() + if wire_name and wire_name in seen_wire_tracks: + continue + try: + route_track = json.loads(route_track_text) + except Exception: + continue + if isinstance(route_track, dict): + route_tracks.append(route_track) + if wire_name: + seen_wire_tracks.add(wire_name) + report = self._select_route_refs(doc, route_tracks, "selected_main_path_detour_route") + report["missing_main_path_detour_route_refs"] = report.pop( + "missing_selected_main_path_detour_route_refs", + [], + ) + return report + + def select_selected_wire_route_sources(self): + doc = _active_document() + selected_wires = [] + seen_names = set() + for obj in list(Gui.Selection.getSelection() or []): + name = getattr(obj, "Name", "") + if name and name not in seen_names: + selected_wires.append(obj) + seen_names.add(name) + for selection in _selection_ex(): + obj = getattr(selection, "Object", None) + name = getattr(obj, "Name", "") + if obj is not None and name and name not in seen_names: + selected_wires.append(obj) + seen_names.add(name) + + route_tracks = [] + missing_refs = [] + for wire in selected_wires: + # 现场排查用:只读取 FreeCAD 导线对象里的路径轨迹,不写数据库、不要求 QET 提供 3D 路径。 + 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: + missing_refs.append(getattr(wire, "Name", "")) + continue + if isinstance(route_track, dict): + route_tracks.append(route_track) + + report = self._select_route_refs(doc, route_tracks, "selected_wire_route") + existing_missing = list(report.get("missing_selected_wire_route_refs", []) or []) + report["missing_selected_wire_route_refs"] = existing_missing + [ + ref for ref in missing_refs if ref and ref not in existing_missing + ] + return report + + def select_selected_wire_rejected_fallback_sources(self): + doc = _active_document() + selected_wires = [] + seen_wire_names = set() + for obj in list(Gui.Selection.getSelection() or []): + name = getattr(obj, "Name", "") + if name and name not in seen_wire_names: + selected_wires.append(obj) + seen_wire_names.add(name) + for selection in _selection_ex(): + obj = getattr(selection, "Object", None) + name = getattr(obj, "Name", "") + if obj is not None and name and name not in seen_wire_names: + selected_wires.append(obj) + seen_wire_names.add(name) + + labels = [] + seen_labels = set() + kinds = [] + seen_kinds = set() + selected_wire_names = [] + invalid_wire_refs = [] + for wire in selected_wires: + # 这里只读导线诊断中被拒绝的兜底路径,不把 fallback 路线自动作为正式布线结果。 + diagnostic_text = str(getattr(wire, "QetRouteDiagnosticsJson", "") or "").strip() + if not diagnostic_text: + continue + try: + diagnostic = json.loads(diagnostic_text) + except Exception: + invalid_wire_refs.append(getattr(wire, "Name", "")) + continue + if not isinstance(diagnostic, dict): + continue + reroute = diagnostic.get("selective_collision_reroute", {}) + if not isinstance(reroute, dict): + continue + wire_name = getattr(wire, "Name", "") + if wire_name and wire_name not in selected_wire_names: + selected_wire_names.append(wire_name) + for kind in list(reroute.get("rejected_fallback_kinds", []) or []): + kind = str(kind or "").strip() + if kind and kind not in seen_kinds: + seen_kinds.add(kind) + kinds.append(kind) + for label in list(reroute.get("rejected_fallback_labels", []) or []): + label = str(label or "").strip() + if label and label not in seen_labels: + seen_labels.add(label) + labels.append(label) + + selected = [] + selected_names = set() + missing_refs = [] + try: + Gui.Selection.clearSelection() + except Exception: + pass + for label in labels: + matches = self._find_objects_by_name_or_label(doc, name=label, label=label) + if not matches: + missing_refs.append(label) + continue + 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) + for ref in invalid_wire_refs: + if ref and ref not in missing_refs: + missing_refs.append(ref) + self.last_report = { + "selected_rejected_fallback_sources": len(selected), + "selected_rejected_fallback_source_names": [getattr(obj, "Name", "") for obj in selected], + "selected_rejected_fallback_wire_names": selected_wire_names, + "rejected_fallback_source_labels": labels, + "rejected_fallback_source_kinds": kinds, + "missing_rejected_fallback_source_refs": missing_refs, + } + return self.last_report + + def select_main_path_detour_rejected_fallback_sources(self): + doc = _active_document() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + detour_summary = summary.get("main_path_detour_missing_summary", {}) + labels = [] + seen_labels = set() + current_route_labels = [] + seen_current_route_labels = set() + kind_counts = {} + label_counts = {} + current_route_label_counts = {} + bridge_pair_counts = {} + if isinstance(detour_summary, dict): + for label in list(detour_summary.get("rejected_fallback_labels", []) or []): + label = str(label or "").strip() + if label and label not in seen_labels: + seen_labels.add(label) + labels.append(label) + raw_current_route_label_counts = detour_summary.get("current_route_source_label_counts", {}) + if isinstance(raw_current_route_label_counts, dict): + current_route_label_counts = { + str(label or "").strip(): int(count or 0) + for label, count in raw_current_route_label_counts.items() + if str(label or "").strip() + } + for label in current_route_label_counts: + if label and label not in seen_current_route_labels: + seen_current_route_labels.add(label) + current_route_labels.append(label) + raw_bridge_pair_counts = detour_summary.get("bridge_pair_counts", {}) + if isinstance(raw_bridge_pair_counts, dict): + bridge_pair_counts = { + str(pair or "").strip(): int(count or 0) + for pair, count in raw_bridge_pair_counts.items() + if str(pair or "").strip() + } + raw_label_counts = detour_summary.get("rejected_fallback_label_counts", {}) + if isinstance(raw_label_counts, dict): + label_counts = { + str(label or "").strip(): int(count or 0) + for label, count in raw_label_counts.items() + if str(label or "").strip() + } + raw_kind_counts = detour_summary.get("rejected_fallback_kind_counts", {}) + if isinstance(raw_kind_counts, dict): + kind_counts = { + str(kind or "").strip(): int(count or 0) + for kind, count in raw_kind_counts.items() + if str(kind or "").strip() + } + + selected = [] + 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 + + @staticmethod + def _find_objects_by_name_or_label(doc, name="", label=""): + refs = [] + seen_names = set() + name = str(name or "").strip() + label = str(label or "").strip() + if name: + obj = doc.getObject(name) + if obj is not None: + refs.append(obj) + seen_names.add(getattr(obj, "Name", "")) + if label: + for candidate in list(getattr(doc, "Objects", []) or []): + candidate_name = getattr(candidate, "Name", "") + if candidate_name in seen_names: + continue + if ( + str(getattr(candidate, "Label", "") or "").strip() == label + or str(getattr(candidate, "QetRouteSourceLabel", "") or "").strip() == label + ): + refs.append(candidate) + seen_names.add(candidate_name) + if name: + for candidate in list(getattr(doc, "Objects", []) or []): + candidate_name = getattr(candidate, "Name", "") + if candidate_name in seen_names: + continue + if ( + candidate_name == name + or str(getattr(candidate, "QetRouteSourceName", "") or "").strip() == name + ): + refs.append(candidate) + 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 {} + ) + 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 []) + + def find_terminal(item): + terminal_uuid = str(item.get("terminal_uuid", "") or "").strip() + name = str(item.get("name", "") or "").strip() + label = str(item.get("label", "") or "").strip() + if name: + obj = doc.getObject(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 label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == label: + return candidate + return None + + selected = [] + missing_refs = [] + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + obj = find_terminal(sample) + if obj is None: + missing_refs.append( + str(sample.get("terminal_uuid", "") or sample.get("name", "") or sample.get("label", "") or "").strip() + ) + continue + try: + Gui.Selection.addSelection(obj) + except Exception: + try: + Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(obj, "Name", "")) + except Exception: + continue + selected.append(obj) + self.last_report = { + "selected_long_terminal_accesses": len(selected), + "selected_long_terminal_names": [getattr(obj, "Name", "") for obj in selected], + "missing_long_terminal_refs": missing_refs, + } + return self.last_report + + 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 []) + selected = [] + selected_names = set() + missing_refs = [] + seen_refs = set() + + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + name = str(sample.get("parent_device_name", "") or "").strip() + label = str(sample.get("parent_device_label", "") or "").strip() + ref = name or label + if not ref or ref in seen_refs: + continue + seen_refs.add(ref) + obj = self._find_object_by_name_or_label(doc, name, label) + if obj is None: + missing_refs.append(ref) + continue + 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) + self.last_report = { + "selected_long_terminal_access_devices": len(selected), + "selected_long_terminal_access_device_names": [getattr(obj, "Name", "") for obj in selected], + "missing_long_terminal_access_device_refs": missing_refs, + } + return self.last_report + + def select_missing_terminal_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 {} + ) + samples = list(batch_payload.get("missing_endpoint_samples", []) or []) + + def find_device(instance_id, element_uuid, device_name="", device_label=""): + obj = TerminalObjects.find_device_group_by_instance_id(doc, instance_id) + if obj is not None: + return obj + obj = TerminalObjects.find_device_group(doc, element_uuid) + if obj is not None: + return obj + instance_id = str(instance_id or "").strip() + element_uuid = str(element_uuid or "").strip() + device_name = str(device_name or "").strip() + device_label = str(device_label or "").strip() + if device_name: + obj = doc.getObject(device_name) + if obj is not None: + return obj + for candidate in list(getattr(doc, "Objects", []) or []): + if instance_id and str(getattr(candidate, "QetInstanceId", "") or "").strip() == instance_id: + return candidate + if element_uuid: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "QetElementUuid", "") or "").strip() == element_uuid: + return candidate + if device_label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == device_label: + return candidate + return None + + selected = [] + selected_names = set() + missing_refs = [] + missing_labels = [] + missing_instance_ids = [] + missing_element_uuids = [] + reason_counts = {} + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + for side in ("start", "end"): + if bool(sample.get("{0}_found".format(side), False)): + continue + terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + instance_id = str(sample.get("{0}_instance_id".format(side), "") or "").strip() + element_uuid = str(sample.get("{0}_element_uuid".format(side), "") or "").strip() + device_name = str(sample.get("{0}_device_name".format(side), "") or "").strip() + device_label = str(sample.get("{0}_device_label".format(side), "") or "").strip() + if instance_id and instance_id not in missing_instance_ids: + missing_instance_ids.append(instance_id) + if element_uuid and element_uuid not in missing_element_uuids: + missing_element_uuids.append(element_uuid) + obj = find_device(instance_id, element_uuid, device_name=device_name, device_label=device_label) + if obj is None: + reason_code = str(sample.get("{0}_missing_endpoint_reason_code".format(side), "") or "").strip() + if reason_code: + reason_counts[reason_code] = reason_counts.get(reason_code, 0) + 1 + missing_refs.append(terminal_uuid or instance_id or element_uuid or device_name or device_label) + label = device_label or device_name or instance_id or element_uuid or terminal_uuid + if label and label not in missing_labels: + missing_labels.append(label) + continue + 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) + self.last_report = { + "selected_missing_terminal_devices": len(selected), + "selected_missing_terminal_device_names": [getattr(obj, "Name", "") for obj in selected], + "missing_terminal_device_refs": missing_refs, + "missing_terminal_device_labels": missing_labels, + "missing_terminal_device_instance_ids": missing_instance_ids, + "missing_terminal_device_element_uuids": missing_element_uuids, + "missing_terminal_device_reason_counts": reason_counts, + } + return self.last_report + + def select_missing_terminal_counterpart_terminals(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("missing_endpoint_samples", []) or []) + + def find_terminal(terminal_uuid="", name="", label=""): + terminal_uuid = str(terminal_uuid or "").strip() + name = str(name or "").strip() + label = str(label or "").strip() + if name: + obj = doc.getObject(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 label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == label: + return candidate + return None + + selected = [] + selected_names = set() + missing_refs = [] + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + sample_selected = False + for side in ("start", "end"): + if not bool(sample.get("{0}_found".format(side), False)): + continue + terminal_uuid = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + terminal_name = str(sample.get("{0}_terminal_name".format(side), "") or "").strip() + terminal_label = str( + sample.get("{0}_terminal_label".format(side), "") + or sample.get("{0}_terminal_display".format(side), "") + or "" + ).strip() + obj = find_terminal(terminal_uuid, terminal_name, terminal_label) + if obj is None: + ref = terminal_uuid or terminal_name or terminal_label + if ref and ref not in missing_refs: + missing_refs.append(ref) + continue + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + sample_selected = True + 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) + sample_selected = True + if not sample_selected: + # 两端都缺失时,把缺失 terminal_uuid 留在报告里,方便和 QET 导线任务对照。 + for side in ("start", "end"): + ref = str(sample.get("{0}_terminal_uuid".format(side), "") or "").strip() + if ref and ref not in missing_refs: + missing_refs.append(ref) + + self.last_report = { + "selected_missing_terminal_counterpart_terminals": len(selected), + "selected_missing_terminal_counterpart_terminal_names": [getattr(obj, "Name", "") for obj in selected], + "missing_terminal_counterpart_refs": missing_refs, + } + return self.last_report + + def select_missing_terminal_candidate_terminals(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("missing_endpoint_samples", []) or []) + + def find_terminal_by_sample(item): + if not isinstance(item, dict): + return None + name = str(item.get("name", "") or "").strip() + label = str(item.get("label", "") or item.get("terminal_label", "") or "").strip() + terminal_uuid = str(item.get("terminal_uuid", "") or "").strip() + if name: + obj = doc.getObject(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 label: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "Label", "") or "").strip() == label: + return candidate + return None + + def fallback_terminals(instance_id, element_uuid): + result = [] + instance_id = str(instance_id or "").strip() + element_uuid = str(element_uuid or "").strip() + for candidate in list(getattr(doc, "Objects", []) or []): + try: + is_terminal = TerminalObjects.is_terminal_object(candidate) + except Exception: + is_terminal = False + if not is_terminal: + continue + if instance_id and str(getattr(candidate, "QetInstanceId", "") or "").strip() == instance_id: + result.append(candidate) + continue + if element_uuid and str(getattr(candidate, "QetElementUuid", "") or "").strip() == element_uuid: + result.append(candidate) + return result + + selected = [] + selected_names = set() + missing_refs = [] + try: + Gui.Selection.clearSelection() + except Exception: + pass + for sample in samples: + if not isinstance(sample, dict): + continue + for side in ("start", "end"): + if bool(sample.get("{0}_found".format(side), False)): + continue + candidate_samples = list(sample.get("{0}_instance_terminal_samples".format(side), []) or []) + if not candidate_samples: + candidate_samples = list(sample.get("{0}_element_terminal_samples".format(side), []) or []) + candidates = [] + for item in candidate_samples: + obj = find_terminal_by_sample(item) + if obj is not None: + candidates.append(obj) + elif isinstance(item, dict): + ref = str( + item.get("name", "") + or item.get("terminal_uuid", "") + or item.get("label", "") + or item.get("terminal_label", "") + or "" + ).strip() + if ref: + missing_refs.append(ref) + if not candidates: + candidates = fallback_terminals( + sample.get("{0}_instance_id".format(side), ""), + sample.get("{0}_element_uuid".format(side), ""), + ) + for obj in candidates: + 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) + self.last_report = { + "selected_missing_terminal_candidate_terminals": len(selected), + "selected_missing_terminal_candidate_terminal_names": [getattr(obj, "Name", "") for obj in selected], + "missing_terminal_candidate_terminal_refs": missing_refs, + } + return self.last_report + + def select_boundary_issue_route_carriers_and_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 {} + ) + if not isinstance(path_payload, dict): + path_payload = {} + + selected = [] + selected_names = set() + selected_carriers = [] + selected_terminals = [] + missing_carriers = [] + missing_terminals = [] + + try: + Gui.Selection.clearSelection() + except Exception: + pass + + def add_object(obj): + if obj is None: + return False + obj_name = getattr(obj, "Name", "") + if obj_name in selected_names: + return False + 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) + return True + + for item in list(path_payload.get("route_carriers_outside_boundary", []) or []): + if not isinstance(item, dict): + continue + carrier_payload = item.get("carrier", {}) if isinstance(item.get("carrier", {}), dict) else {} + name = str(carrier_payload.get("name", "") or "").strip() + label = str(carrier_payload.get("label", "") or "").strip() + obj = self._find_object_by_name_or_label(doc, name, label) + if obj is None: + ref = name or label + if ref: + missing_carriers.append(ref) + continue + if add_object(obj): + selected_carriers.append(obj) + + for item in list(path_payload.get("terminals_outside_boundary", []) or []): + if not isinstance(item, dict): + continue + name = str(item.get("name", "") or "").strip() + label = str(item.get("label", "") or "").strip() + terminal_uuid = str(item.get("terminal_uuid", "") or "").strip() + obj = self._find_object_by_name_or_label(doc, name, label) + if obj is None and terminal_uuid: + for candidate in list(getattr(doc, "Objects", []) or []): + if str(getattr(candidate, "QetTerminalUuid", "") or "").strip() == terminal_uuid: + obj = candidate + break + if obj is None: + ref = name or terminal_uuid or label + if ref: + missing_terminals.append(ref) + continue + if add_object(obj): + selected_terminals.append(obj) + + self.last_report = { + "selected_boundary_objects": len(selected), + "selected_boundary_route_carriers": len(selected_carriers), + "selected_boundary_route_carrier_names": [getattr(obj, "Name", "") for obj in selected_carriers], + "selected_boundary_terminals": len(selected_terminals), + "selected_boundary_terminal_names": [getattr(obj, "Name", "") for obj in selected_terminals], + "missing_boundary_route_carrier_refs": missing_carriers, + "missing_boundary_terminal_refs": missing_terminals, + } + return self.last_report + def generate_layout_space(self): """Prepare the whole document as an EPLAN-style layout space.""" doc = _active_document() @@ -245,47 +1846,316 @@ class AutoRoutingController: } return self.last_report - def route_eplan_connections(self): + def set_selected_terminal_local_route_points(self): + result = RoutingNetwork.set_terminal_local_route_points_from_selection(_selection_ex()) + terminal = result.get("terminal") + self.last_report = { + "terminal_local_routes": 1, + "terminal_local_route_names": [getattr(terminal, "Name", "")] if terminal is not None else [], + "terminal_local_route_labels": [getattr(terminal, "Label", "")] if terminal is not None else [], + "terminal_local_route_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() - payload = getattr(App, "_qet_exchange_payload", None) - report = AutoRouting.route_eplan_connections( + created = RoutingNetwork.create_user_path_bridge_from_selection( doc, - payload=payload if isinstance(payload, dict) and payload.get("wires") else None, - options=self.routing_options(), + _selection_ex(), project_uuid=project_uuid, - update_network=True, ) - if report.get("total_wires", 0) <= 0: - raise AutoRoutingPanelError( - "没有导线任务。请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中存在导线任务。" - ) - self.last_report = report - return report + self.last_report = { + "user_path_bridges": len(created), + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report - def clear_routing_connections(self): + def create_user_path_bridges_from_main_path_detour_pairs(self): doc = _active_document() - removed = AutoRouting.clear_routing_connections(doc) - self.last_report = {"removed": removed} - return removed + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + summary = AutoRouting.collect_routing_diagnostic_summary(doc) + detour_summary = summary.get("main_path_detour_missing_summary", {}) + pair_counts = detour_summary.get("bridge_pair_counts", {}) if isinstance(detour_summary, dict) else {} + if not isinstance(pair_counts, dict): + pair_counts = {} + + created = [] + missing_pairs = [] + duplicates = 0 + for pair_text, _count in sorted(pair_counts.items(), key=lambda item: (-int(item[1] or 0), str(item[0]))): + pair_text = str(pair_text or "").strip() + if " -> " not in pair_text: + continue + left_label, right_label = [part.strip() for part in pair_text.split(" -> ", 1)] + if not left_label or not right_label: + continue + left_matches = self._find_objects_by_name_or_label(doc, name=left_label, label=left_label) + right_matches = self._find_objects_by_name_or_label(doc, name=right_label, label=right_label) + if not left_matches or not right_matches: + missing_pairs.append(pair_text) + continue + new_bridges = RoutingNetwork.create_user_path_bridge_between_objects( + doc, + left_matches[0], + right_matches[0], + project_uuid=project_uuid, + ) + if new_bridges: + created.extend(new_bridges) + else: + duplicates += 1 - def clear_route_carriers(self): - doc = _active_document() - removed = RoutingNetwork.clear_route_carriers(doc) - self.last_report = {"removed_carriers": removed} - return removed + self.last_report = { + "main_path_detour_bridge_pairs": len(pair_counts), + "main_path_detour_user_path_bridges": len(created), + "main_path_detour_bridge_duplicates": duplicates, + "missing_main_path_detour_bridge_pairs": missing_pairs, + } + return self.last_report - def save(self): + def create_user_path_bridges_from_diagnostic_suggestions(self): doc = _active_document() - file_name = getattr(doc, "FileName", "") - if file_name and hasattr(doc, "save"): - doc.save() - if ExchangeWriteBack is not None: - return ExchangeWriteBack.write_back_document(doc) - return {"saved": bool(file_name)} - - -class AutoRoutingTaskPanel: + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + total_created = 0 + total_suggestions = 0 + total_duplicates = 0 + total_stale = 0 + diagnostic_passes = 0 + max_passes = 5 + for _index in range(max_passes): + diagnostic_report = AutoRouting.check_eplan_routing_path_network( + doc, + project_uuid=project_uuid, + options=self.routing_options(), + ) + diagnostic_passes += 1 + bridge_report = RoutingNetwork.create_user_path_bridges_from_diagnostic_suggestions( + doc, + diagnostic_report.get("diagnostic", {}), + project_uuid=project_uuid, + ) + created_count = len(bridge_report.get("created", []) or []) + total_created += created_count + total_suggestions += int(bridge_report.get("suggestions", 0) or 0) + total_duplicates += int(bridge_report.get("duplicates", 0) or 0) + total_stale += int(bridge_report.get("stale_suggestions", 0) or 0) + # 新桥接会改变路径组件关系;继续诊断一轮,处理链式接入建议。 + if created_count <= 0: + 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) + self.last_report = { + "user_path_bridges": total_created + detour_created, + "diagnostic_suggestions": total_suggestions, + "duplicate_bridges": total_duplicates, + "stale_suggestions": total_stale, + "diagnostic_passes": diagnostic_passes, + "main_path_detour_bridge_pairs": int(detour_bridge_report.get("main_path_detour_bridge_pairs", 0) or 0), + "main_path_detour_user_path_bridges": detour_created, + "main_path_detour_bridge_duplicates": int(detour_bridge_report.get("main_path_detour_bridge_duplicates", 0) or 0), + "missing_main_path_detour_bridge_pairs": list( + detour_bridge_report.get("missing_main_path_detour_bridge_pairs", []) or [] + ), + "routing_path_network_checked": True, + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + + def mark_cabinet_boundary_from_selection(self): + doc = _active_document() + marked = RoutingNetwork.mark_cabinet_interior_boundaries_from_selection(_selection_ex()) + self.last_report = { + "cabinet_boundary_objects": len(marked), + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + + def _mark_selected_objects_obstacle_mode(self, mode): + doc = _active_document() + marked = RoutingNetwork.mark_obstacle_mode_from_selection(_selection_ex(), mode) + self.last_report = { + "obstacle_mode": str(mode or "").strip(), + "obstacle_mode_objects": len(marked), + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + + def mark_selected_objects_pass_through_obstacle(self): + return self._mark_selected_objects_obstacle_mode("PassThrough") + + def restore_selected_objects_as_obstacles(self): + return self._mark_selected_objects_obstacle_mode("") + + def mark_selected_route_carriers_forbidden(self): + return self._mark_selected_route_carriers_constraint("Forbidden") + + def mark_selected_route_carriers_required(self): + return self._mark_selected_route_carriers_constraint("Required") + + def clear_selected_route_carrier_constraints(self): + return self._mark_selected_route_carriers_constraint("") + + def set_selected_route_carriers_capacity(self): + doc = _active_document() + selection = _selection_ex() + capacity = int(self.routing_options().get("selected_route_capacity", 1) or 1) + report = RoutingNetwork.set_route_carrier_capacity_from_selection( + doc, + selection, + capacity, + ) + report["network"] = RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ) + self.last_report = report + return report + + def clear_all_route_carrier_constraints(self): + doc = _active_document() + report = RoutingNetwork.clear_all_route_constraint_modes(doc) + report["network"] = RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ) + self.last_report = report + return report + + def _mark_selected_route_carriers_constraint(self, mode): + doc = _active_document() + selection = _selection_ex() + selected_sources = [] + source_modes_before = {} + seen_sources = set() + for item in selection or []: + source = getattr(item, "Object", None) + if source is None or RoutingNetwork.is_route_carrier(source) or id(source) in seen_sources: + continue + seen_sources.add(id(source)) + selected_sources.append(source) + source_modes_before[id(source)] = (getattr(source, "QetRouteConstraintMode", "") or "").strip() + marked = RoutingNetwork.mark_route_constraint_mode_from_selection( + doc, + selection, + mode, + ) + source_count = 0 + for source in selected_sources: + current_mode = (getattr(source, "QetRouteConstraintMode", "") or "").strip() + if mode: + if current_mode == mode: + source_count += 1 + continue + if source_modes_before.get(id(source), ""): + source_count += 1 + self.last_report = { + "route_constraint_mode": mode, + "route_constraint_carriers": len(marked), + "route_constraint_sources": source_count, + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + + def route_eplan_connections(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + payload = getattr(App, "_qet_exchange_payload", None) + report = AutoRouting.route_eplan_connections( + doc, + payload=payload if isinstance(payload, dict) and payload.get("wires") else None, + options=self.routing_options(), + project_uuid=project_uuid, + update_network=True, + ) + if report.get("total_wires", 0) <= 0: + raise AutoRoutingPanelError( + "没有导线任务。请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中存在导线任务。" + ) + self.last_report = report + return report + + def clear_routing_connections(self): + doc = _active_document() + removed = AutoRouting.clear_routing_connections(doc) + self.last_report = {"removed": removed} + return removed + + def clear_route_carriers(self): + doc = _active_document() + removed = RoutingNetwork.clear_route_carriers(doc) + self.last_report = {"removed_carriers": removed} + return removed + + def save(self): + doc = _active_document() + file_name = getattr(doc, "FileName", "") + if file_name and hasattr(doc, "save"): + doc.save() + if ExchangeWriteBack is not None: + return ExchangeWriteBack.write_back_document(doc) + return {"saved": bool(file_name)} + + +class AutoRoutingTaskPanel: def __init__(self, controller=None): if QtWidgets is None: raise AutoRoutingPanelError("Qt widgets are not available.") @@ -327,6 +2197,39 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.terminal_access_max_distance_spin) + options_layout.addWidget(QtWidgets.QLabel("端子接入警告距离 mm")) + self.terminal_access_warning_distance_spin = QtWidgets.QDoubleSpinBox() + self.terminal_access_warning_distance_spin.setRange(0.0, 100000.0) + self.terminal_access_warning_distance_spin.setDecimals(1) + self.terminal_access_warning_distance_spin.setSingleStep(50.0) + self.terminal_access_warning_distance_spin.setToolTip( + "超过该长度的端子接入会标记为过长;0 表示按最大距离和默认值自动计算。" + ) + self.terminal_access_warning_distance_spin.setValue( + float( + self.controller.routing_options().get( + "terminal_access_warning_distance", + AutoRouting.DEFAULT_OPTIONS["terminal_access_warning_distance"], + ) + ) + ) + options_layout.addWidget(self.terminal_access_warning_distance_spin) + options_layout.addWidget(QtWidgets.QLabel("预检抽样数")) + self.preflight_routeability_sample_limit_spin = QtWidgets.QSpinBox() + self.preflight_routeability_sample_limit_spin.setRange(0, 100000) + self.preflight_routeability_sample_limit_spin.setSingleStep(5) + self.preflight_routeability_sample_limit_spin.setToolTip( + "检查布线准备度时最多抽样求解多少条导线可达性;0 表示关闭可达性抽样,默认关闭。" + ) + self.preflight_routeability_sample_limit_spin.setValue( + int( + self.controller.routing_options().get( + "preflight_routeability_sample_limit", + AutoRouting.DEFAULT_OPTIONS["preflight_routeability_sample_limit"], + ) + ) + ) + options_layout.addWidget(self.preflight_routeability_sample_limit_spin) options_layout.addWidget(QtWidgets.QLabel("自动端子出线长度 mm")) self.terminal_exit_length_spin = QtWidgets.QDoubleSpinBox() self.terminal_exit_length_spin.setRange(0.0, 1000.0) @@ -341,6 +2244,40 @@ class AutoRoutingTaskPanel: ) ) options_layout.addWidget(self.terminal_exit_length_spin) + options_layout.addWidget(QtWidgets.QLabel("障碍安全间隙 mm")) + self.obstacle_clearance_spin = QtWidgets.QDoubleSpinBox() + self.obstacle_clearance_spin.setRange(0.0, 1000.0) + self.obstacle_clearance_spin.setDecimals(1) + self.obstacle_clearance_spin.setSingleStep(1.0) + self.obstacle_clearance_spin.setToolTip( + "用于膨胀障碍包围盒;导线进入膨胀范围但未穿过原始障碍时会标记为安全间隙告警。" + ) + self.obstacle_clearance_spin.setValue( + float( + self.controller.routing_options().get( + "obstacle_clearance", + AutoRouting.DEFAULT_OPTIONS["obstacle_clearance"], + ) + ) + ) + options_layout.addWidget(self.obstacle_clearance_spin) + options_layout.addWidget(QtWidgets.QLabel("共路复用惩罚")) + self.segment_reuse_penalty_spin = QtWidgets.QDoubleSpinBox() + self.segment_reuse_penalty_spin.setRange(0.0, 100000.0) + self.segment_reuse_penalty_spin.setDecimals(1) + self.segment_reuse_penalty_spin.setSingleStep(50.0) + self.segment_reuse_penalty_spin.setToolTip( + "多根导线复用超过 carrier 容量的路径段时增加的搜索成本;调高会更倾向绕到备用路径。" + ) + self.segment_reuse_penalty_spin.setValue( + float( + self.controller.routing_options().get( + "segment_reuse_penalty", + AutoRouting.DEFAULT_OPTIONS["segment_reuse_penalty"], + ) + ) + ) + options_layout.addWidget(self.segment_reuse_penalty_spin) options_layout.addWidget(QtWidgets.QLabel("并行线间距 mm")) self.lane_spacing_spin = QtWidgets.QDoubleSpinBox() self.lane_spacing_spin.setRange(0.0, 1000.0) @@ -382,6 +2319,13 @@ class AutoRoutingTaskPanel: axis_index = self.lane_axis_combo.findText(lane_axis) self.lane_axis_combo.setCurrentIndex(axis_index if axis_index >= 0 else 0) options_layout.addWidget(self.lane_axis_combo) + options_layout.addWidget(QtWidgets.QLabel("选中路径容量")) + self.selected_route_capacity_spin = QtWidgets.QSpinBox() + self.selected_route_capacity_spin.setRange(1, 999) + self.selected_route_capacity_spin.setValue( + int(self.controller.routing_options().get("selected_route_capacity", 4) or 4) + ) + options_layout.addWidget(self.selected_route_capacity_spin) self.generate_layout_button = _style_command_button( QtWidgets.QPushButton(), @@ -398,96 +2342,371 @@ class AutoRoutingTaskPanel: self.create_user_paths_button = _style_command_button( QtWidgets.QPushButton(), "选中路径作为用户路径", - "把选中的草图、线段或纯线状对象转换为可参与自动布线的 UserPath。", + "把选中的草图、Draft 线、曲线、Wire 或纯线状对象转换为可参与自动布线的 UserPath;同时选中支撑面 Face 时会投影到该面附近。", ) - self.check_paths_button = _style_command_button( + self.set_terminal_local_route_button = _style_command_button( QtWidgets.QPushButton(), - "检查布线路径网络", - "检查 routing path network 的断点、孤立网络和未接入端子,并写入诊断对象。", + "选中端子设置局部出线", + "选中一个可布线端子和一条草图/Draft 局部路径,把路径写入端子的 QetTerminalLocalRoutePointsJson;不写数据库。", ) - self.route_connections_button = _style_command_button( + self.create_user_path_bridge_button = _style_command_button( QtWidgets.QPushButton(), - "生成布线连接", - "自动更新布线路径网络,并按全部 QET 导线任务生成 3D 布线连接。", + "选中两路径生成桥接", + "选中两个已生成的布线路径 carrier,或它们的源路径对象,自动在最近点之间生成一段 UserPath 桥接。", ) - self.clear_routes_button = _style_command_button(QtWidgets.QPushButton(), "清除布线连接") - self.clear_carriers_button = _style_command_button(QtWidgets.QPushButton(), "清除走线路径") - self.save_button = _style_command_button(QtWidgets.QPushButton(), "保存") + self.create_diagnostic_bridges_button = _style_command_button( + QtWidgets.QPushButton(), + "按诊断建议生成桥接", + "先刷新布线路径网络诊断,再按 wire_ducts_without_terminal_access 的 bridge_suggestion 生成 UserPath 桥接。", + ) - self.status_label = QtWidgets.QLabel("") - self.status_label.setWordWrap(True) + self.mark_cabinet_boundary_button = _style_command_button( + QtWidgets.QPushButton(), + "选中对象作为柜内边界", + "把选中的柜内空间或柜体对象包围盒作为 CabinetInterior,用于限制自动布线路径不要跑出柜内区域。", + ) - for widget in ( - self.generate_layout_button, - self.create_user_paths_button, - self.generate_paths_button, - self.check_paths_button, - self.route_connections_button, - self.clear_routes_button, - self.clear_carriers_button, - self.save_button, - ): - layout.addWidget(widget) + self.select_boundary_issue_objects_button = _style_command_button( + QtWidgets.QPushButton(), + "选择越界路径/端子", + "从最新路径网络诊断中选择越出 CabinetInterior 的路径 carrier 和工程端子,便于修正 UserPath、边界或设备位置。", + ) - layout.addLayout(options_layout) - layout.addWidget(self.status_label) + self.mark_pass_through_obstacle_button = _style_command_button( + QtWidgets.QPushButton(), + "选中对象忽略碰撞", + "把确认可穿越或不参与布线碰撞的支架、柜体辅助件等标记为 PassThrough;不会改数据库。", + ) - self.generate_paths_button.clicked.connect(self.generate_routing_paths) - self.create_user_paths_button.clicked.connect(self.create_user_paths_from_selection) - self.check_paths_button.clicked.connect(self.check_routing_path_network) - self.generate_layout_button.clicked.connect(self.generate_layout_space) - self.route_connections_button.clicked.connect(self.route_eplan_connections) - self.clear_routes_button.clicked.connect(self.clear_routing_connections) - self.clear_carriers_button.clicked.connect(self.clear_route_carriers) - self.save_button.clicked.connect(self.save) + self.select_top_collision_obstacles_button = _style_command_button( + QtWidgets.QPushButton(), + "选择高发碰撞对象", + "从最新批量布线诊断中选择 top_collision_obstacles,便于确认后再标记忽略碰撞或调整路径。", + ) - self._refresh_status() + self.select_device_collision_obstacles_button = _style_command_button( + QtWidgets.QPushButton(), + "选择设备碰撞对象", + "只选择诊断为设备/布局碰撞的高发对象,便于补设备局部路径、调整线槽入口或检查装配。", + ) - def _refresh_status(self): - try: - self._set_status(self.controller.summary()) - except Exception as exc: - self._set_error(str(exc)) + self.select_collision_parent_assemblies_button = _style_command_button( + QtWidgets.QPushButton(), + "选择碰撞父装配", + "从最新批量布线诊断中选择高发碰撞对象的父装配,适合门板/柜体总成确认后统一忽略碰撞。", + ) - def _set_status(self, message): - self.status_label.setText(message) - _console_message(message) + self.select_structural_collision_parent_assemblies_button = _style_command_button( + QtWidgets.QPushButton(), + "选择结构件碰撞父装配", + "只选择疑似柜体/门板/支架结构件的碰撞父装配,确认后再标记 PassThrough,避免误选真实设备。", + ) - def _set_error(self, message): - self.status_label.setText(message) - _console_error(message) + self.mark_structural_collision_parent_assemblies_button = _style_command_button( + QtWidgets.QPushButton(), + "确认结构件忽略碰撞", + "按最新诊断只把疑似柜体/门板/支架等结构件碰撞父装配标记为 PassThrough;不会标记疑似设备碰撞。", + ) - def _sync_options_from_widgets(self): - self.controller.set_adjoining_duct_tolerance(self.adjoining_duct_tolerance_spin.value()) - self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value()) - self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value()) - self.controller.set_lane_spacing(self.lane_spacing_spin.value()) - self.controller.set_lane_max_offset(self.lane_max_offset_spin.value()) - self.controller.set_lane_axis(self.lane_axis_combo.currentText()) + self.select_collision_wires_button = _style_command_button( + QtWidgets.QPushButton(), + "选择碰撞导线", + "从最新批量布线诊断中选择带 collision_warnings 的导线对象,便于和高发碰撞对象一起核对。", + ) - def generate_routing_paths(self): - try: - self._sync_options_from_widgets() - result = self.controller.generate_routing_paths() - wire_ducts = result.get("wire_duct_carriers", 0) - user_paths = result.get("user_path_carriers", 0) - surfaces = result.get("surface_carriers", 0) - terminal_access = result.get("terminal_access_carriers", 0) - network = result.get("network", {}) if isinstance(result.get("network", {}), dict) else {} - if network.get("segments", 0) == 0: - self._set_status( - "未生成可用布线路径网络。可选中线槽实体、草图/线段作为用户路径,或确认安装板/背板能作为布线区域。" - + self.controller.summary() - ) - return - self._set_status( - "已生成布线路径网络:线槽路径 {0} 条,用户路径 {1} 条,布线区域 {2} 条,端子接入 {3} 条,网络段 {4} 条。{5}".format( - wire_ducts, - user_paths, - surfaces, + self.select_main_path_detour_missing_wires_button = _style_command_button( + QtWidgets.QPushButton(), + "选择缺主路径导线", + "选择诊断为 main_path_detour_missing 的导线,用于补 UserPath、桥接主路径或调整装配。", + ) + + self.select_main_path_detour_missing_route_sources_button = _style_command_button( + QtWidgets.QPushButton(), + "选择缺主路径线路径", + "从缺主路径导线样例中选择当前实际经过的路径 carrier 和源草图/线槽,用于对照需补路径位置。", + ) + + self.select_main_path_detour_rejected_fallback_sources_button = _style_command_button( + QtWidgets.QPushButton(), + "选择缺主路径补路位置", + "从汇总诊断的需补路径位置中反选被拒绝的 RoutingRange/AuxiliaryPath 来源对象。", + ) + + self.select_issue_wires_button = _style_command_button( + QtWidgets.QPushButton(), + "选择异常导线", + "从最新批量布线诊断中选择 route_samples 里带 issue_codes 的导线对象,用于统一排查长接入、越界、容量和碰撞等问题。", + ) + + self.select_issue_route_sources_button = _style_command_button( + QtWidgets.QPushButton(), + "选择异常导线路径", + "从最新异常导线样例中选择实际经过的路径 carrier 和源草图/线槽,便于调整 UserPath、容量或路径约束。", + ) + + self.select_selected_wire_route_sources_button = _style_command_button( + QtWidgets.QPushButton(), + "选择选中导线路径", + "先选中一根或多根已生成导线,再从导线 QetRouteTrackJson 反选其经过的路径 carrier 和源草图/线槽。", + ) + + self.select_selected_wire_rejected_fallback_sources_button = _style_command_button( + QtWidgets.QPushButton(), + "选择拒绝兜底路径", + "先选中缺主路径导线,再定位其诊断中被拒绝的 RoutingRange/AuxiliaryPath 来源,用于补 UserPath 或桥接。", + ) + + self.select_long_terminal_accesses_button = _style_command_button( + QtWidgets.QPushButton(), + "选择长接入端子", + "从最新批量布线诊断中选择 long_terminal_accesses 端子,便于检查设备装配高度或局部出线路径。", + ) + + self.select_long_terminal_access_devices_button = _style_command_button( + QtWidgets.QPushButton(), + "选择长接入设备", + "从最新批量布线诊断中选择长接入端子所属设备,便于检查设备高度和局部出线路径。", + ) + + self.select_missing_terminal_devices_button = _style_command_button( + QtWidgets.QPushButton(), + "选择缺端子设备", + "从最新批量布线诊断中选择缺失端子所在的 3D 设备,便于补工程端子或检查 2D/3D 绑定。", + ) + + self.select_missing_terminal_counterpart_terminals_button = _style_command_button( + QtWidgets.QPushButton(), + "选择缺端子另一端", + "从缺端子导线样例中选择已找到的另一端工程端子,便于对照缺失侧设备和端子脚号。", + ) + + self.select_missing_terminal_candidate_terminals_button = _style_command_button( + QtWidgets.QPushButton(), + "选择缺端子候选端子", + "从最新缺端子诊断中选择同设备/同实例已有的工程端子,用于核对 terminal_uuid 或脚号绑定是否错位。", + ) + + self.restore_obstacle_button = _style_command_button( + QtWidgets.QPushButton(), + "选中对象恢复障碍", + "清除选中对象的碰撞忽略标记,使其重新参与导线碰撞检测。", + ) + + self.mark_route_required_button = _style_command_button( + QtWidgets.QPushButton(), + "选中路径必须经过", + "把选中的布线路径 carrier 标记为 Required;如果选中源路径对象,会同步标记它生成的全部路径。", + ) + + self.mark_route_forbidden_button = _style_command_button( + QtWidgets.QPushButton(), + "选中路径禁止经过", + "把选中的布线路径 carrier 标记为 Forbidden;如果选中源路径对象,会同步标记它生成的全部路径。", + ) + + self.clear_route_constraint_button = _style_command_button( + QtWidgets.QPushButton(), + "清除选中路径约束", + "清除选中布线路径 carrier 的 Required/Forbidden 约束;如果选中源路径对象,会同步清除它生成的全部路径。", + ) + + self.set_route_capacity_button = _style_command_button( + QtWidgets.QPushButton(), + "选中路径设置容量", + "把选中布线路径 carrier 或源路径对象的容量设置为上面的数值,用于容量压力诊断和共路复用惩罚。", + ) + + self.clear_all_route_constraints_button = _style_command_button( + QtWidgets.QPushButton(), + "清除全部路径约束", + "清除当前文档里所有布线路径 carrier 和源路径对象的 Required/Forbidden 约束。", + ) + + self.check_paths_button = _style_command_button( + QtWidgets.QPushButton(), + "检查布线路径网络", + "检查 routing path network 的断点、孤立网络和未接入端子,并写入诊断对象。", + ) + + self.check_readiness_button = _style_command_button( + QtWidgets.QPushButton(), + "检查布线准备度", + "检查导线任务、工程端子、路径网络和导线样式库是否已满足自动布线输入条件。", + ) + + self.route_connections_button = _style_command_button( + QtWidgets.QPushButton(), + "生成布线连接", + "自动更新布线路径网络,并按全部 QET 导线任务生成 3D 布线连接。", + ) + + self.diagnostic_summary_button = _style_command_button( + QtWidgets.QPushButton(), + "汇总布线诊断", + "汇总预检、路径网络和批量布线的最新诊断对象,便于手动测试后统一复盘。", + ) + + self.clear_routes_button = _style_command_button(QtWidgets.QPushButton(), "清除布线连接") + self.clear_carriers_button = _style_command_button(QtWidgets.QPushButton(), "清除走线路径") + self.save_button = _style_command_button(QtWidgets.QPushButton(), "保存") + + self.status_label = QtWidgets.QLabel("") + self.status_label.setWordWrap(True) + + for widget in ( + self.generate_layout_button, + self.create_user_paths_button, + self.set_terminal_local_route_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_top_collision_obstacles_button, + self.select_device_collision_obstacles_button, + self.select_collision_parent_assemblies_button, + self.select_structural_collision_parent_assemblies_button, + self.mark_structural_collision_parent_assemblies_button, + self.select_collision_wires_button, + 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_issue_wires_button, + self.select_issue_route_sources_button, + self.select_selected_wire_route_sources_button, + self.select_selected_wire_rejected_fallback_sources_button, + self.select_long_terminal_accesses_button, + self.select_long_terminal_access_devices_button, + self.select_missing_terminal_devices_button, + self.select_missing_terminal_counterpart_terminals_button, + self.select_missing_terminal_candidate_terminals_button, + self.mark_pass_through_obstacle_button, + self.restore_obstacle_button, + self.mark_route_required_button, + self.mark_route_forbidden_button, + self.clear_route_constraint_button, + self.set_route_capacity_button, + self.clear_all_route_constraints_button, + self.generate_paths_button, + self.check_paths_button, + self.check_readiness_button, + self.route_connections_button, + self.diagnostic_summary_button, + self.clear_routes_button, + self.clear_carriers_button, + self.save_button, + ): + layout.addWidget(widget) + + layout.addLayout(options_layout) + layout.addWidget(self.status_label) + + self.generate_paths_button.clicked.connect(self.generate_routing_paths) + 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.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_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) + self.select_structural_collision_parent_assemblies_button.clicked.connect( + self.select_structural_collision_parent_assemblies + ) + self.mark_structural_collision_parent_assemblies_button.clicked.connect( + self.mark_structural_collision_parent_assemblies_pass_through + ) + self.select_collision_wires_button.clicked.connect(self.select_collision_wires) + self.select_main_path_detour_missing_wires_button.clicked.connect(self.select_main_path_detour_missing_wires) + self.select_main_path_detour_missing_route_sources_button.clicked.connect( + self.select_main_path_detour_missing_route_sources + ) + self.select_main_path_detour_rejected_fallback_sources_button.clicked.connect( + self.select_main_path_detour_rejected_fallback_sources + ) + 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) + self.select_selected_wire_rejected_fallback_sources_button.clicked.connect( + self.select_selected_wire_rejected_fallback_sources + ) + self.select_long_terminal_accesses_button.clicked.connect(self.select_long_terminal_accesses) + self.select_long_terminal_access_devices_button.clicked.connect(self.select_long_terminal_access_devices) + self.select_missing_terminal_devices_button.clicked.connect(self.select_missing_terminal_devices) + self.select_missing_terminal_counterpart_terminals_button.clicked.connect( + self.select_missing_terminal_counterpart_terminals + ) + self.select_missing_terminal_candidate_terminals_button.clicked.connect( + self.select_missing_terminal_candidate_terminals + ) + self.mark_pass_through_obstacle_button.clicked.connect(self.mark_selected_objects_pass_through_obstacle) + self.restore_obstacle_button.clicked.connect(self.restore_selected_objects_as_obstacles) + self.mark_route_required_button.clicked.connect(self.mark_selected_route_carriers_required) + self.mark_route_forbidden_button.clicked.connect(self.mark_selected_route_carriers_forbidden) + self.clear_route_constraint_button.clicked.connect(self.clear_selected_route_carrier_constraints) + self.set_route_capacity_button.clicked.connect(self.set_selected_route_carriers_capacity) + self.clear_all_route_constraints_button.clicked.connect(self.clear_all_route_carrier_constraints) + self.check_paths_button.clicked.connect(self.check_routing_path_network) + self.check_readiness_button.clicked.connect(self.check_routing_readiness) + self.generate_layout_button.clicked.connect(self.generate_layout_space) + self.route_connections_button.clicked.connect(self.route_eplan_connections) + self.diagnostic_summary_button.clicked.connect(self.collect_routing_diagnostic_summary) + self.clear_routes_button.clicked.connect(self.clear_routing_connections) + self.clear_carriers_button.clicked.connect(self.clear_route_carriers) + self.save_button.clicked.connect(self.save) + + self._refresh_status() + + def _refresh_status(self): + try: + self._set_status(self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def _set_status(self, message): + self.status_label.setText(message) + _console_message(message) + + def _set_error(self, message): + self.status_label.setText(message) + _console_error(message) + + def _sync_options_from_widgets(self): + self.controller.set_adjoining_duct_tolerance(self.adjoining_duct_tolerance_spin.value()) + self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value()) + self.controller.set_terminal_access_warning_distance(self.terminal_access_warning_distance_spin.value()) + self.controller.set_preflight_routeability_sample_limit(self.preflight_routeability_sample_limit_spin.value()) + self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value()) + self.controller.set_obstacle_clearance(self.obstacle_clearance_spin.value()) + self.controller.set_segment_reuse_penalty(self.segment_reuse_penalty_spin.value()) + self.controller.set_lane_spacing(self.lane_spacing_spin.value()) + self.controller.set_lane_max_offset(self.lane_max_offset_spin.value()) + self.controller.set_lane_axis(self.lane_axis_combo.currentText()) + self.controller.set_selected_route_capacity(self.selected_route_capacity_spin.value()) + + def generate_routing_paths(self): + try: + self._sync_options_from_widgets() + result = self.controller.generate_routing_paths() + wire_ducts = result.get("wire_duct_carriers", 0) + user_paths = result.get("user_path_carriers", 0) + surfaces = result.get("surface_carriers", 0) + terminal_access = result.get("terminal_access_carriers", 0) + network = result.get("network", {}) if isinstance(result.get("network", {}), dict) else {} + if network.get("segments", 0) == 0: + self._set_status( + "未生成可用布线路径网络。可选中线槽实体、草图/线段作为用户路径,或确认安装板/背板能作为布线区域。" + + self.controller.summary() + ) + return + self._set_status( + "已生成布线路径网络:线槽路径 {0} 条,用户路径 {1} 条,布线区域 {2} 条,端子接入 {3} 条,网络段 {4} 条。{5}".format( + wire_ducts, + user_paths, + surfaces, terminal_access, network.get("segments", 0), self.controller.summary(), @@ -508,7 +2727,7 @@ class AutoRoutingTaskPanel: stale_text = "已清理失效用户路径:{0} 条。".format(removed) self._set_status( stale_text - + "未创建用户路径。请先选择草图、Draft 线、线段或纯线状对象。" + + "未生成/刷新用户路径。请先选择草图、Draft 线、曲线、Wire 或纯线状对象;如果路径悬空,可同时选中安装板/柜板 Face。" + self.controller.summary() ) return @@ -516,7 +2735,7 @@ class AutoRoutingTaskPanel: if removed > 0: stale_text = "已清理失效用户路径:{0} 条。".format(removed) self._set_status( - "{0}已创建用户路径:{1} 条。{2}".format( + "{0}已生成/刷新用户路径:{1} 条。{2}".format( stale_text, created, self.controller.summary(), @@ -525,6 +2744,687 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def set_selected_terminal_local_route_points(self): + try: + result = self.controller.set_selected_terminal_local_route_points() + count = result.get("terminal_local_routes", 0) + point_count = result.get("terminal_local_route_point_count", 0) + labels = list(result.get("terminal_local_route_labels", []) or []) + display = labels[0] if labels else "" + if count <= 0: + self._set_status( + "未设置端子局部出线。请同时选中一个可布线端子和一条草图/Draft 局部路径。" + + self.controller.summary() + ) + return + self._set_status( + "已设置端子局部出线:{0},路径点 {1} 个。重新生成布线路径网络后会参与自动布线。{2}".format( + display or "选中端子", + point_count, + 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() + result = self.controller.create_user_path_bridge_from_selection() + created = result.get("user_path_bridges", 0) + if created <= 0: + self._set_status( + "未生成桥接 UserPath。请先选择两个已生成的布线路径 carrier,或选择能找到 live carrier 的源路径对象。" + + self.controller.summary() + ) + return + self._set_status( + "已生成桥接 UserPath:{0} 条。{1}".format(created, self.controller.summary()) + ) + except Exception as exc: + self._set_error(str(exc)) + + def create_user_path_bridges_from_diagnostic_suggestions(self): + try: + self._sync_options_from_widgets() + result = self.controller.create_user_path_bridges_from_diagnostic_suggestions() + created = result.get("user_path_bridges", 0) + suggestions = result.get("diagnostic_suggestions", 0) + duplicates = result.get("duplicate_bridges", 0) + stale = result.get("stale_suggestions", 0) + detour_pairs = result.get("main_path_detour_bridge_pairs", 0) + 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 []) + if created <= 0: + detour_text = "" + if detour_pairs: + detour_text = " 缺主路径配对 {0} 条,已存在 {1} 条。".format( + int(detour_pairs or 0), + int(detour_duplicates or 0), + ) + missing_text = "" + if missing_detour_pairs: + missing_text = " 未找到配对:{0}。".format("、".join(missing_detour_pairs[:3])) + self._set_status( + "未按诊断建议生成桥接。建议 {0} 条,已存在 {1} 条,失效 {2} 条。{3}{4}请先点击“检查布线路径网络”或“汇总布线诊断”确认是否存在可桥接建议。{5}".format( + suggestions, + duplicates, + stale, + detour_text, + missing_text, + self.controller.summary(), + ) + ) + return + detour_text = "" + if detour_pairs or detour_created: + detour_text = " 缺主路径配对 {0} 条,生成 {1} 条。".format( + int(detour_pairs or 0), + int(detour_created or 0), + ) + self._set_status( + "已按诊断建议生成桥接 UserPath:{0} 条。建议 {1} 条,已存在 {2} 条,失效 {3} 条。{4}{5}".format( + created, + suggestions, + duplicates, + stale, + detour_text, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def mark_cabinet_boundary_from_selection(self): + try: + result = self.controller.mark_cabinet_boundary_from_selection() + marked = result.get("cabinet_boundary_objects", 0) + if marked <= 0: + self._set_status( + "未标记柜内边界。请先选择带几何包围盒的柜内空间、柜体或辅助实体。" + + self.controller.summary() + ) + return + self._set_status( + "已标记柜内边界:{0} 个。{1}".format(marked, self.controller.summary()) + ) + except Exception as exc: + self._set_error(str(exc)) + + def select_boundary_issue_route_carriers_and_terminals(self): + try: + result = self.controller.select_boundary_issue_route_carriers_and_terminals() + carriers = result.get("selected_boundary_route_carriers", 0) + terminals = result.get("selected_boundary_terminals", 0) + missing_carriers = list(result.get("missing_boundary_route_carrier_refs", []) or []) + missing_terminals = list(result.get("missing_boundary_terminal_refs", []) or []) + if carriers <= 0 and terminals <= 0: + self._set_status( + "未选择越界路径/端子。请先点击“检查布线路径网络”,并确认诊断中存在路径越出柜内边界或端子越出柜内边界。" + + self.controller.summary() + ) + return + message = "已选择越界路径 {0} 条、越界端子 {1} 个。".format(carriers, terminals) + missing = missing_carriers + missing_terminals + 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", "忽略碰撞") + + def select_top_collision_obstacles(self): + try: + result = self.controller.select_top_collision_obstacles() + selected = result.get("selected_collision_obstacles", 0) + missing = list(result.get("missing_collision_obstacle_names", []) or []) + if selected <= 0: + self._set_status( + "未选择高发碰撞对象。请先生成布线连接并查看批量诊断里的 top_collision_obstacles。" + + self.controller.summary() + ) + return + message = "已选择高发碰撞对象:{0} 个。".format(selected) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + message += "请确认这些对象是否可穿越,再点击“选中对象忽略碰撞”或调整路径。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_top_collision_parent_assemblies(self): + try: + result = self.controller.select_top_collision_parent_assemblies() + selected = result.get("selected_collision_parent_assemblies", 0) + missing = list(result.get("missing_collision_parent_assembly_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择碰撞父装配。请先生成布线连接,并确认 top_collision_obstacles 中存在 parent_names/parent_labels。" + + self.controller.summary() + ) + return + message = "已选择碰撞父装配:{0} 个。".format(selected) + if missing: + message += " 未找到父装配:{0}。".format("、".join(missing[:5])) + message += "请确认这些总成可穿越,再点击“选中对象忽略碰撞”;该标记会递归作用到下层导入子件。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_device_or_layout_collision_obstacles(self): + try: + result = self.controller.select_device_or_layout_collision_obstacles() + selected = result.get("selected_device_or_layout_collision_obstacles", 0) + missing = list(result.get("missing_device_or_layout_collision_obstacle_names", []) or []) + if selected <= 0: + self._set_status( + "未选择设备/布局碰撞对象。请先生成布线连接,并确认 top_collision_obstacles 中存在疑似设备或装配碰撞。" + + self.controller.summary() + ) + return + message = "已选择设备/布局碰撞对象:{0} 个。".format(selected) + 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 select_structural_collision_parent_assemblies(self): + try: + result = self.controller.select_structural_collision_parent_assemblies() + selected = result.get("selected_structural_collision_parent_assemblies", 0) + missing = list(result.get("missing_structural_collision_parent_assembly_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择结构件碰撞父装配。请先生成布线连接并汇总诊断,确认存在疑似柜体/门板/支架结构件碰撞。" + + self.controller.summary() + ) + return + message = "已选择结构件碰撞父装配:{0} 个。".format(selected) + if missing: + message += " 未找到父装配:{0}。".format("、".join(missing[:5])) + message += "请确认这些结构件可穿越,再点击“选中对象忽略碰撞”。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def mark_structural_collision_parent_assemblies_pass_through(self): + try: + result = self.controller.mark_structural_collision_parent_assemblies_pass_through() + marked = result.get("marked_structural_collision_parent_assemblies", 0) + missing = list(result.get("missing_structural_collision_parent_assembly_refs", []) or []) + if marked <= 0: + self._set_status( + "未标记结构件忽略碰撞。请先生成布线连接并汇总诊断,确认 top_collision_obstacles 中存在结构件处理建议。" + + self.controller.summary() + ) + return + message = "已把结构件碰撞父装配标记为忽略碰撞:{0} 个。".format(marked) + if missing: + message += " 未找到父装配:{0}。".format("、".join(missing[:5])) + message += "请重新生成布线连接,查看结构件碰撞是否减少。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_collision_wires(self): + try: + result = self.controller.select_collision_wires() + selected = result.get("selected_collision_wires", 0) + missing = list(result.get("missing_collision_wire_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择碰撞导线。请先生成布线连接,并确认批量诊断中存在 collision_warnings。" + + self.controller.summary() + ) + return + message = "已选择碰撞导线:{0} 条。".format(selected) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + message += "可结合“选择高发碰撞对象”判断是结构件误报还是需要补路径。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_main_path_detour_missing_wires(self): + try: + result = self.controller.select_main_path_detour_missing_wires() + selected = result.get("selected_main_path_detour_missing_wires", 0) + missing = list(result.get("missing_main_path_detour_missing_wire_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择缺主路径导线。请先生成布线连接,并确认诊断中存在 main_path_detour_missing。" + + self.controller.summary() + ) + return + message = "已选择缺主路径导线:{0} 条。".format(selected) + 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 select_main_path_detour_missing_route_sources(self): + try: + result = self.controller.select_main_path_detour_missing_route_sources() + carriers = result.get("selected_main_path_detour_route_carriers", 0) + sources = result.get("selected_main_path_detour_route_sources", 0) + missing = list(result.get("missing_main_path_detour_route_refs", []) or []) + if carriers <= 0 and sources <= 0: + self._set_status( + "未选择缺主路径线路径。请先生成布线连接,并确认 route_samples 中存在 main_path_detour_missing 和 route_track。" + + self.controller.summary() + ) + return + message = "已选择缺主路径线当前路径:carrier {0} 条,源对象 {1} 个。".format(carriers, sources) + 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 select_main_path_detour_rejected_fallback_sources(self): + try: + result = self.controller.select_main_path_detour_rejected_fallback_sources() + selected = result.get( + "selected_main_path_detour_bridge_endpoint_objects", + result.get("selected_main_path_detour_rejected_fallback_sources", 0), + ) + selected_fallback = result.get("selected_main_path_detour_rejected_fallback_sources", 0) + selected_current = result.get("selected_main_path_detour_current_route_sources", 0) + missing = list(result.get("missing_main_path_detour_rejected_fallback_refs", []) or []) + missing_current = list(result.get("missing_main_path_detour_current_route_refs", []) or []) + labels = list(result.get("main_path_detour_rejected_fallback_labels", []) or []) + label_counts = result.get("main_path_detour_rejected_fallback_label_counts", {}) + if not isinstance(label_counts, dict): + label_counts = {} + current_label_counts = result.get("main_path_detour_current_route_source_label_counts", {}) + if not isinstance(current_label_counts, dict): + current_label_counts = {} + bridge_pair_counts = result.get("main_path_detour_bridge_pair_counts", {}) + if not isinstance(bridge_pair_counts, dict): + bridge_pair_counts = {} + kind_counts = result.get("main_path_detour_rejected_fallback_kind_counts", {}) + if not isinstance(kind_counts, dict): + kind_counts = {} + if selected <= 0: + self._set_status( + "未选择缺主路径补路位置。请先生成布线连接并点击“汇总布线诊断”,确认存在 main_path_detour_missing 和需补路径位置。" + + self.controller.summary() + ) + return + message = "已选择缺主路径补路位置:{0} 个。".format(selected) + if selected_current > 0: + message += " 其中兜底区域 {0} 个,当前主路径 {1} 个。".format( + int(selected_fallback or 0), + int(selected_current or 0), + ) + location_items = [] + if label_counts: + location_items = [ + "{0} {1} 条".format(str(label), int(count or 0)) + for label, count in sorted(label_counts.items(), key=lambda item: (-int(item[1] or 0), str(item[0])))[:5] + if int(count or 0) > 0 + ] + if not location_items and labels: + location_items = [str(label) for label in labels[:5]] + if location_items: + message += " 位置:{0}。".format("、".join(location_items)) + current_items = [] + if current_label_counts: + current_items = [ + "{0} {1} 条".format(str(label), int(count or 0)) + for label, count in sorted(current_label_counts.items(), key=lambda item: (-int(item[1] or 0), str(item[0])))[:5] + if int(count or 0) > 0 + ] + if current_items: + message += " 当前主路径:{0}。".format("、".join(current_items)) + pair_items = [] + if bridge_pair_counts: + pair_items = [ + "{0} {1} 条".format(str(pair), int(count or 0)) + for pair, count in sorted(bridge_pair_counts.items(), key=lambda item: (-int(item[1] or 0), str(item[0])))[:5] + if int(count or 0) > 0 + ] + if pair_items: + message += " 补路配对:{0}。".format("、".join(pair_items)) + if kind_counts: + kind_text = "、".join( + "{0} {1} 处".format(str(kind), int(count or 0)) + for kind, count in sorted(kind_counts.items()) + if int(count or 0) > 0 + ) + if kind_text: + message += " 类型:{0}。".format(kind_text) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + if missing_current: + message += " 未找到当前主路径:{0}。".format("、".join(missing_current[:5])) + message += "请在这些区域补 UserPath、桥接主路径或完善设备局部出线路径。" + 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() + selected = result.get("selected_issue_wires", 0) + missing = list(result.get("missing_issue_wire_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择异常导线。请先生成布线连接,并确认批量诊断 route_samples 中存在 issue_codes。" + + self.controller.summary() + ) + return + message = "已选择异常导线:{0} 条。".format(selected) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + message += "请查看导线属性 QetRouteIssueCodes / QetRouteIssueLabels,再按长接入、越界、容量或碰撞分类处理。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_issue_route_sources(self): + try: + result = self.controller.select_issue_route_sources() + carriers = result.get("selected_issue_route_carriers", 0) + sources = result.get("selected_issue_route_sources", 0) + missing = list(result.get("missing_issue_route_refs", []) or []) + if carriers <= 0 and sources <= 0: + self._set_status( + "未选择异常导线路径。请先生成布线连接,并确认批量诊断 route_samples 中存在 issue_codes 和 route_track/carrier_names。" + + self.controller.summary() + ) + return + message = "已选择异常导线路径:carrier {0} 条,源对象 {1} 个。".format(carriers, sources) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + message += "可检查这些 UserPath/线槽是否越界、穿模、容量不足,或按需要设置 Required/Forbidden。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_selected_wire_route_sources(self): + try: + result = self.controller.select_selected_wire_route_sources() + carriers = result.get("selected_wire_route_carriers", 0) + sources = result.get("selected_wire_route_sources", 0) + missing = list(result.get("missing_selected_wire_route_refs", []) or []) + if carriers <= 0 and sources <= 0: + self._set_status( + "未选择选中导线路径。请先选中已生成的 3D 导线,并确认导线属性 QetRouteTrackJson 中存在 carrier/source 信息。" + + self.controller.summary() + ) + return + message = "已选择选中导线路径:carrier {0} 条,源对象 {1} 个。".format(carriers, sources) + if missing: + message += " 未找到或轨迹无效:{0}。".format("、".join(missing[:5])) + message += "可检查这些路径是否越界、穿模、容量不足,或按需要设置 Required/Forbidden。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_selected_wire_rejected_fallback_sources(self): + try: + result = self.controller.select_selected_wire_rejected_fallback_sources() + selected = result.get("selected_rejected_fallback_sources", 0) + missing = list(result.get("missing_rejected_fallback_source_refs", []) or []) + labels = list(result.get("rejected_fallback_source_labels", []) or []) + if selected <= 0: + self._set_status( + "未选择拒绝兜底路径。请先选中带 main_path_detour_missing 的导线,并确认其 QetRouteDiagnosticsJson 中存在 rejected_fallback_labels。" + + self.controller.summary() + ) + return + message = "已选择拒绝兜底路径来源:{0} 个。".format(selected) + if labels: + message += " 需补路径位置:{0}。".format("、".join(str(label) for label in labels[:5])) + 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 select_long_terminal_accesses(self): + try: + result = self.controller.select_long_terminal_accesses() + selected = result.get("selected_long_terminal_accesses", 0) + missing = list(result.get("missing_long_terminal_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择长接入端子。请先生成布线连接或检查布线路径网络,并确认诊断中存在 long_terminal_accesses。" + + self.controller.summary() + ) + return + message = "已选择长接入端子:{0} 个。".format(selected) + if missing: + message += " 未找到:{0}。".format("、".join(missing[:5])) + message += "请检查端子位置、设备装配高度,或为设备补局部出线路径。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_long_terminal_access_devices(self): + try: + result = self.controller.select_long_terminal_access_devices() + selected = result.get("selected_long_terminal_access_devices", 0) + missing = list(result.get("missing_long_terminal_access_device_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择长接入设备。请先生成布线连接或检查布线路径网络,并确认 long_terminal_accesses 中存在 parent_device_name/label。" + + self.controller.summary() + ) + return + message = "已选择长接入设备:{0} 个。".format(selected) + if missing: + message += " 未找到设备:{0}。".format("、".join(missing[:5])) + message += "请检查设备装配高度、端子 LCS 位置/方向,或为设备补局部出线路径。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_missing_terminal_devices(self): + try: + result = self.controller.select_missing_terminal_devices() + selected = result.get("selected_missing_terminal_devices", 0) + missing = list(result.get("missing_terminal_device_refs", []) or []) + missing_labels = list(result.get("missing_terminal_device_labels", []) or []) + missing_instances = list(result.get("missing_terminal_device_instance_ids", []) or []) + reason_counts = result.get("missing_terminal_device_reason_counts", {}) + if not isinstance(reason_counts, dict): + reason_counts = {} + if selected <= 0: + if int(reason_counts.get("device_not_in_3d_scene", 0) or 0) > 0: + message = ( + "未选择缺端子设备:缺失侧 2D 设备未在当前 FreeCAD 场景中找到。" + "请先检查设备是否已导入、装配并完成 2D/3D 绑定。" + ) + display_missing = missing_labels or missing + if display_missing: + message += " 未找到:{0}。".format("、".join(display_missing[:5])) + if missing_instances: + message += " instance_id:{0}。".format("、".join(missing_instances[:5])) + self._set_status(message + self.controller.summary()) + return + if int(reason_counts.get("missing_device_binding_metadata", 0) or 0) > 0: + message = ( + "未选择缺端子设备:QET 导线端点缺少 element_uuid," + "FreeCAD 无法判断缺失端子属于哪个 2D 设备;第一版不要求 start/end_instance_id。" + ) + if missing: + message += " 缺失端点:{0}。".format("、".join(missing[:5])) + self._set_status(message + self.controller.summary()) + return + self._set_status( + "未选择缺端子设备。请先生成布线连接,并确认批量诊断中存在 missing_endpoint_samples。" + + self.controller.summary() + ) + return + message = "已选择缺端子设备:{0} 个。".format(selected) + display_missing = missing_labels or missing + if display_missing: + message += " 未找到设备:{0}。".format("、".join(display_missing[:5])) + if missing_instances: + message += " instance_id:{0}。".format("、".join(missing_instances[:5])) + message += "请在这些设备上补工程端子,或检查 QET 端子 UUID 与 3D 实例绑定。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_missing_terminal_counterpart_terminals(self): + try: + result = self.controller.select_missing_terminal_counterpart_terminals() + selected = result.get("selected_missing_terminal_counterpart_terminals", 0) + missing = list(result.get("missing_terminal_counterpart_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择缺端子另一端。请先生成布线连接,并确认缺端子样例中至少一端已经找到工程端子。" + + self.controller.summary() + ) + return + message = "已选择缺端子另一端端子:{0} 个。".format(selected) + if missing: + message += " 未找到或两端都缺失:{0}。".format("、".join(missing[:5])) + message += "请对照这些已找到端子,检查缺失侧设备是否生成工程端子、terminal_uuid 是否匹配。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def select_missing_terminal_candidate_terminals(self): + try: + result = self.controller.select_missing_terminal_candidate_terminals() + selected = result.get("selected_missing_terminal_candidate_terminals", 0) + missing = list(result.get("missing_terminal_candidate_terminal_refs", []) or []) + if selected <= 0: + self._set_status( + "未选择缺端子候选端子。请先重新生成布线连接,确认 missing_endpoint_samples 中存在同设备端子样例。" + + self.controller.summary() + ) + return + message = "已选择缺端子候选端子:{0} 个。".format(selected) + if missing: + message += " 未找到候选端子:{0}。".format("、".join(missing[:5])) + message += "请核对这些 3D 工程端子的 terminal_uuid、脚号和 QET 导线端点是否一致。" + self._set_status(message + self.controller.summary()) + except Exception as exc: + self._set_error(str(exc)) + + def restore_selected_objects_as_obstacles(self): + self._mark_selected_objects_obstacle_mode("", "恢复障碍") + + def _mark_selected_objects_obstacle_mode(self, mode, label): + try: + result = self.controller._mark_selected_objects_obstacle_mode(mode) + marked = result.get("obstacle_mode_objects", 0) + if marked <= 0: + self._set_status( + "未{0}。请先选择需要处理的实体对象,不要选择已生成的布线路径 carrier。".format(label) + + self.controller.summary() + ) + return + self._set_status( + "已{0}:对象 {1} 个。{2}".format( + label, + marked, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def mark_selected_route_carriers_required(self): + self._mark_selected_route_carriers_constraint("Required", "必须经过") + + def mark_selected_route_carriers_forbidden(self): + self._mark_selected_route_carriers_constraint("Forbidden", "禁止经过") + + def clear_selected_route_carrier_constraints(self): + self._mark_selected_route_carriers_constraint("", "约束清除") + + def set_selected_route_carriers_capacity(self): + try: + self._sync_options_from_widgets() + result = self.controller.set_selected_route_carriers_capacity() + marked = result.get("route_capacity_carriers", 0) + sources = result.get("route_capacity_sources", 0) + capacity = result.get("route_capacity", 0) + if marked <= 0 and sources <= 0: + self._set_status( + "未设置路径容量。请先选择已生成的布线路径 carrier,或选择草图/Draft/线槽/支撑面等源路径对象。" + + self.controller.summary() + ) + return + self._set_status( + "已设置路径容量:容量 {0},carrier {1} 条,源路径 {2} 个。{3}".format( + capacity, + marked, + sources, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def clear_all_route_carrier_constraints(self): + try: + result = self.controller.clear_all_route_carrier_constraints() + carriers = result.get("route_constraint_carriers", 0) + sources = result.get("route_constraint_sources", 0) + if carriers <= 0 and sources <= 0: + self._set_status("当前没有路径约束需要清除。{0}".format(self.controller.summary())) + return + self._set_status( + "已清除全部路径约束:carrier {0} 条,源路径 {1} 个。{2}".format( + carriers, + sources, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def _mark_selected_route_carriers_constraint(self, mode, label): + try: + result = self.controller._mark_selected_route_carriers_constraint(mode) + marked = result.get("route_constraint_carriers", 0) + sources = result.get("route_constraint_sources", 0) + if marked <= 0 and sources <= 0: + action_text = "清除" if not mode else "标记" + self._set_status( + "未{0}路径约束。请先选择已生成的布线路径 carrier,或选择草图/Draft 等源路径对象。".format( + action_text + ) + + self.controller.summary() + ) + return + if not mode: + self._set_status( + "已清除选中路径约束:carrier {0} 条,源路径 {1} 个。{2}".format( + marked, + sources, + self.controller.summary(), + ) + ) + return + self._set_status( + "已标记路径{0}:carrier {1} 条,源路径 {2} 个。{3}".format( + label, + marked, + sources, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + def check_routing_path_network(self): try: self._sync_options_from_widgets() @@ -539,6 +3439,14 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def check_routing_readiness(self): + try: + self._sync_options_from_widgets() + report = self.controller.check_routing_readiness() + self._set_status(AutoRouting.format_eplan_routing_preflight_report(report)) + except Exception as exc: + self._set_error(str(exc)) + def generate_layout_space(self): try: result = self.controller.generate_layout_space() @@ -570,6 +3478,13 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def collect_routing_diagnostic_summary(self): + try: + report = self.controller.collect_routing_diagnostic_summary() + self._set_status(AutoRouting.format_routing_diagnostic_summary(report)) + except Exception as exc: + self._set_error(str(exc)) + def clear_routing_connections(self): try: removed = self.controller.clear_routing_connections() diff --git a/src/Mod/FreeCADExchange/ExchangeBootstrap.py b/src/Mod/FreeCADExchange/ExchangeBootstrap.py index 45081e1..23010cb 100644 --- a/src/Mod/FreeCADExchange/ExchangeBootstrap.py +++ b/src/Mod/FreeCADExchange/ExchangeBootstrap.py @@ -1,6 +1,7 @@ import json import traceback import os +import sqlite3 from pathlib import Path import FreeCAD as App @@ -405,6 +406,15 @@ def _optional_string(item, field_name, entry_label): return value.strip() if isinstance(value, str) else "" +def _optional_text(item, field_name): + value = item.get(field_name, "") + if value is None: + return "" + if isinstance(value, str): + return value.strip() + return str(value).strip() + + def _normalize_conductor_uuids(item, entry_label): values = item.get("conductor_uuids", []) if values is None: @@ -455,8 +465,10 @@ def _normalize_wires(payload): "wire_id": wire_id, "net_uuid": _optional_string(item, "net_uuid", entry_label), "group_uuid": _optional_string(item, "group_uuid", entry_label), + "wire_label": _optional_string(item, "wire_label", entry_label), "wire_mark": _optional_string(item, "wire_mark", entry_label), "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), @@ -558,6 +570,68 @@ def _normalize_cabinet(payload): return normalized +def _has_wire_properties_table(database_path): + try: + connection = sqlite3.connect(str(database_path)) + except Exception: + return False + try: + row = connection.execute( + """ + SELECT name + FROM sqlite_master + WHERE type = 'table' + AND name = 'wire_properties' + LIMIT 1 + """ + ).fetchone() + return row is not None + except Exception: + return False + finally: + try: + connection.close() + except Exception: + pass + + +def _wire_style_database_path(payload, json_path): + for key in ("wire_style_database_path", "project_database_path", "database_path"): + value = payload.get(key, "") + if isinstance(value, str) and value.strip(): + return value.strip() + try: + directory = Path(json_path).resolve().parent + except Exception: + return "" + candidates = [] + search_dirs = [] + for base in (directory, directory.parent, directory.parent.parent): + if base and base not in search_dirs: + search_dirs.append(base) + data_dir = base / "datafiles" + if data_dir not in search_dirs: + search_dirs.append(data_dir) + for base in (directory.parent, directory.parent.parent): + try: + for data_dir in base.glob("*/datafiles"): + if data_dir not in search_dirs: + search_dirs.append(data_dir) + except Exception: + pass + for search_dir in search_dirs: + for pattern in ("project-local.db", "project-local.sqlite", "*.sqlite", "*.sqlite3", "*.db"): + try: + candidates.extend(search_dir.glob(pattern)) + except Exception: + pass + for candidate in sorted(set(candidates), key=lambda item: item.name.lower()): + # 不要求 QET 改协议;FreeCAD 只在候选库确实含 wire_properties 时自动使用。 + if _has_wire_properties_table(candidate): + return str(candidate) + return "" + + def load_exchange_payload(json_path): try: raw_text = Path(json_path).read_text(encoding="utf-8") @@ -594,6 +668,9 @@ def load_exchange_payload(json_path): "device_models": _normalize_device_models(payload), "wires": _normalize_wires(payload), } + wire_style_database_path = _wire_style_database_path(payload, json_path) + if wire_style_database_path: + normalized["wire_style_database_path"] = wire_style_database_path return normalized @@ -623,6 +700,7 @@ def _build_summary(payload, json_path): "missing_terminal_instances": missing_terminal_instances, "cabinet": cabinet, "scene_path": os.environ.get(ENV_SCENE_PATH, "").strip(), + "wire_style_database_path": payload.get("wire_style_database_path", ""), } diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 1157dec..e087838 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -23,6 +23,10 @@ ROUTE_CARRIER_KIND_USER_PATH = "UserPath" ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath" ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange" ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess" +ROUTING_BOUNDARY_ROLE = "RoutingBoundary" +ROUTING_BOUNDARY_KIND_CABINET_INTERIOR = "CabinetInterior" +ROUTE_CONSTRAINT_MODE_REQUIRED = "Required" +ROUTE_CONSTRAINT_MODE_FORBIDDEN = "Forbidden" BRIDGEABLE_ENDPOINT_CARRIER_KINDS = { ROUTE_CARRIER_KIND, ROUTE_CARRIER_KIND_WIRE_DUCT, @@ -45,11 +49,13 @@ DEFAULT_SURFACE_MARGIN = 20.0 DEFAULT_WIRE_DUCT_MARGIN = 20.0 DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH = 20.0 DEFAULT_ROUTE_PATH_FACE_OFFSET = 2.0 +DEFAULT_USER_PATH_EDGE_DISCRETIZE_DEFLECTION = 5.0 DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT = 2.5 DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE = 1000.0 DEFAULT_TERMINAL_ACCESS_WARNING_DISTANCE = 500.0 DEFAULT_TERMINAL_ACCESS_COMPONENT_SEGMENT_PENALTY = 25.0 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_ADJOINING_DUCT_TOLERANCE = 5.0 DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 @@ -106,12 +112,16 @@ SUPPORT_SURFACE_NAME_KEYWORDS = ( "door panel", "rear door", "front door", + "side cover", + "side panel", "cabinet face", "cabinet panel", "\u5b89\u88c5\u677f", "\u80cc\u677f", "\u5e95\u677f", "\u95e8\u677f", + "\u4fa7\u76d6", + "\u4fa7\u677f", "\u67dc\u9762", ) SUPPORT_SURFACE_CARRIER_KINDS = { @@ -226,6 +236,21 @@ def _distance(left, right): return (dx * dx + dy * dy + dz * dz) ** 0.5 +def _placement_mult_vec(placement, point): + if placement is None: + return point + try: + transformed = placement.multVec(point) + if transformed is not None: + return _vector(transformed) + except Exception: + pass + base = getattr(placement, "Base", None) + if base is not None: + return App.Vector(point.x + base.x, point.y + base.y, point.z + base.z) + return point + + def _add(left, right): return App.Vector( float(left.x) + float(right.x), @@ -403,6 +428,43 @@ def _point_payload(point): } +def _bbox_axis_value(bbox, attr_name, dict_name): + if isinstance(bbox, dict): + return float(bbox[dict_name]) + return float(getattr(bbox, attr_name)) + + +def _point_inside_bbox(point, bbox, tolerance=0.000001): + if point is None or bbox is None: + return False + try: + point = _vector(point) + return ( + _bbox_axis_value(bbox, "XMin", "xmin") - tolerance + <= float(point.x) + <= _bbox_axis_value(bbox, "XMax", "xmax") + tolerance + and _bbox_axis_value(bbox, "YMin", "ymin") - tolerance + <= float(point.y) + <= _bbox_axis_value(bbox, "YMax", "ymax") + tolerance + and _bbox_axis_value(bbox, "ZMin", "zmin") - tolerance + <= float(point.z) + <= _bbox_axis_value(bbox, "ZMax", "zmax") + tolerance + ) + except Exception: + return False + + +def _point_inside_any_bbox(point, bboxes): + return any(_point_inside_bbox(point, bbox) for bbox in bboxes or []) + + +def _segment_inside_any_bbox(start, end, bboxes): + if not bboxes: + return True + # 柜内区域采用包围盒语义;一段线必须整体落在同一个柜内盒里才进入优先路径图。 + return any(_point_inside_bbox(start, bbox) and _point_inside_bbox(end, bbox) for bbox in bboxes or []) + + def _is_finite_point(point): try: return all( @@ -520,6 +582,27 @@ def _route_carrier_capacity_value(obj, default=1): return int(default or 1) +def _normalized_route_capacity(capacity): + try: + normalized = int(float(capacity or 0)) + except Exception: + normalized = 1 + return max(normalized, 1) + + +def _set_route_carrier_capacity_value(obj, capacity): + if obj is None: + return _normalized_route_capacity(capacity) + normalized = _normalized_route_capacity(capacity) + _ensure_integer_property( + obj, + "QetRouteCarrierCapacity", + "How many routed wires can reuse this carrier segment before detouring is preferred", + normalized, + ) + return normalized + + def _wire_duct_end_margin_value(source, default=DEFAULT_WIRE_DUCT_MARGIN): try: value = float(getattr(source, "QetWireDuctEndMarginMm", default) or 0.0) @@ -617,6 +700,68 @@ def _set_user_path_source_semantics(source): ) +def _object_has_bbox(obj): + shape = getattr(obj, "Shape", None) + return getattr(shape, "BoundBox", None) is not None + + +def _set_cabinet_interior_boundary_semantics(source): + if source is None: + return + TerminalObjects.ensure_string_property( + source, + "QetRoutingRole", + PROPERTY_GROUP, + "Routing role marker", + ROUTING_BOUNDARY_ROLE, + ) + TerminalObjects.ensure_string_property( + source, + "QetRoutingBoundaryKind", + PROPERTY_GROUP, + "Routing boundary kind", + ROUTING_BOUNDARY_KIND_CABINET_INTERIOR, + ) + TerminalObjects.ensure_string_property( + source, + "QetRoutingObstacleMode", + PROPERTY_GROUP, + "How routing connection collision checks should treat this object", + WIRE_DUCT_OBSTACLE_MODE, + ) + + +def _set_routing_obstacle_mode(source, mode): + if source is None: + return + normalized = str(mode or "").strip() + TerminalObjects.ensure_string_property( + source, + "QetRoutingObstacleMode", + PROPERTY_GROUP, + "How routing connection collision checks should treat this object", + normalized, + ) + + +def set_routing_obstacle_mode(source, mode): + _set_routing_obstacle_mode(source, mode) + return source + + +def mark_obstacle_mode_from_selection(selection_ex, mode): + marked = [] + seen = set() + for item in selection_ex or []: + source = getattr(item, "Object", None) + if source is None or is_route_carrier(source) or id(source) in seen: + continue + set_routing_obstacle_mode(source, mode) + seen.add(id(source)) + marked.append(source) + return marked + + def _style_route_carrier(carrier, kind): style = ROUTE_CARRIER_VIEW_STYLES.get(kind) or ROUTE_CARRIER_VIEW_STYLES[ROUTE_CARRIER_KIND] try: @@ -744,6 +889,16 @@ def is_route_carrier(obj): return False +def is_routing_boundary(obj): + if obj is None: + return False + boundary_kind = (getattr(obj, "QetRoutingBoundaryKind", "") or "").strip() + if boundary_kind in {ROUTING_BOUNDARY_KIND_CABINET_INTERIOR, ROUTING_BOUNDARY_ROLE}: + return True + role = (getattr(obj, "QetRoutingRole", "") or "").strip() + return role == ROUTING_BOUNDARY_ROLE + + def _carrier_points(obj): points = list(getattr(obj, "Points", []) or []) if points: @@ -1010,6 +1165,16 @@ def _shape_center(shape): def _edge_points(edge): + discretize = getattr(edge, "discretize", None) + if callable(discretize): + try: + # 草图弧线/样条需要离散成折线,否则会被首尾点直接拉直。 + points = [_vector(point) for point in discretize(Deflection=DEFAULT_USER_PATH_EDGE_DISCRETIZE_DEFLECTION)] + if len(points) >= 2: + return points + except Exception: + pass + first = None last = None vertexes = list(getattr(edge, "Vertexes", []) or []) @@ -1027,9 +1192,64 @@ def _edge_points(edge): return [] +def _wire_points(wire): + discretize = getattr(wire, "discretize", None) + if callable(discretize): + try: + # 整条 Wire 的拓扑顺序通常比逐条 Edge 更稳定,优先用它保留草图路径走向。 + points = [_vector(point) for point in discretize(Deflection=DEFAULT_USER_PATH_EDGE_DISCRETIZE_DEFLECTION)] + if len(points) >= 2: + return points + except Exception: + pass + + points = [] + for edge in list(getattr(wire, "Edges", []) or []): + points.extend(_edge_points(edge)) + return points + + +def _object_global_placement(obj): + if obj is None: + return None + try: + if hasattr(obj, "getGlobalPlacement"): + placement = obj.getGlobalPlacement() + if placement is not None: + return placement + except Exception: + pass + return getattr(obj, "Placement", None) + + +def _points_with_placement(points, placement): + return [_placement_mult_vec(placement, _vector(point)) for point in points] + + +def _is_valid_route_source_bbox(bbox, max_extent=1.0e9): + if bbox is None: + return True + for axis in ("x", "y", "z"): + try: + extent = float(_bbox_extent(bbox, axis)) + except Exception: + return False + if not math.isfinite(extent) or abs(extent) > float(max_extent or 0.0): + return False + return True + + def _is_route_path_source_object(obj): if obj is None: return False + if is_route_carrier(obj): + return False + if is_routing_boundary(obj): + return False + name = str(getattr(obj, "Name", "") or "").lower() + label = str(getattr(obj, "Label", "") or "").lower() + if name.startswith(("x_axis", "y_axis", "z_axis")) or label.startswith(("x轴", "y轴", "z轴")): + return False type_id = (getattr(obj, "TypeId", "") or "").lower() if "sketch" in type_id: return True @@ -1038,6 +1258,8 @@ def _is_route_path_source_object(obj): shape = getattr(obj, "Shape", None) if shape is None: return False + if not _is_valid_route_source_bbox(getattr(shape, "BoundBox", None)): + return False # SOLIDWORKS/EPLAN 的 routing path 是草图/线槽路径,不是把实体零件的全部边都当路径。 # 所以只有纯线状对象才允许整对象转换;带 Face/Solid 的实体必须显式选中边。 faces = list(getattr(shape, "Faces", []) or []) @@ -1045,7 +1267,7 @@ def _is_route_path_source_object(obj): shells = list(getattr(shape, "Shells", []) or []) if faces or solids or shells: return False - return bool(list(getattr(shape, "Edges", []) or [])) + return bool(list(getattr(shape, "Wires", []) or []) or list(getattr(shape, "Edges", []) or [])) def _routing_source_text(obj): @@ -1076,7 +1298,7 @@ def _bbox_aspect_ratio(bbox): def _is_wire_duct_candidate(obj, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT): if obj is None: return False - if is_route_carrier(obj) or TerminalObjects.is_terminal_object(obj): + if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): return False if (getattr(obj, "RouteType", "") or "").strip(): return False @@ -1121,7 +1343,7 @@ def _is_thin_surface_bbox(bbox, min_surface_extent=50.0, max_thickness=40.0, thi def _is_support_surface_candidate(obj): if obj is None: return False - if is_route_carrier(obj) or TerminalObjects.is_terminal_object(obj): + if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): return False if (getattr(obj, "RouteType", "") or "").strip(): return False @@ -1148,7 +1370,7 @@ def _is_support_surface_candidate(obj): def _is_wiring_cut_out_candidate(obj): if obj is None: return False - if is_route_carrier(obj) or TerminalObjects.is_terminal_object(obj): + if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): return False if (getattr(obj, "RouteType", "") or "").strip(): return False @@ -1202,45 +1424,80 @@ def _support_face_from_bbox(bbox): return _BBoxFace(points, normal) -def _points_from_selection_item(selection_item): +def _normalize_point_run(points): + normalized = [] + for point in points or []: + if not normalized or _distance(normalized[-1], point) > DEFAULT_NODE_TOLERANCE: + normalized.append(point) + return normalized + + +def _point_runs_from_selection_item(selection_item): + runs = [] points = [] + obj = getattr(selection_item, "Object", None) + placement = _object_global_placement(obj) for point in list(getattr(selection_item, "PickedPoints", []) or []): points.append(_vector(point)) for sub_object in list(getattr(selection_item, "SubObjects", []) or []): shape_type = (getattr(sub_object, "ShapeType", "") or "").lower() + if shape_type == "wire": + if points: + runs.append(points) + points = [] + runs.append(_points_with_placement(_wire_points(sub_object), placement)) + continue if shape_type == "edge": - points.extend(_edge_points(sub_object)) + points.extend(_points_with_placement(_edge_points(sub_object), placement)) continue if shape_type == "vertex": point = getattr(sub_object, "Point", None) if point is not None: - points.append(_vector(point)) + points.append(_placement_mult_vec(placement, _vector(point))) continue center = _shape_center(sub_object) if center is not None: - points.append(center) + points.append(_placement_mult_vec(placement, center)) - obj = getattr(selection_item, "Object", None) if obj is not None and _is_route_path_source_object(obj): for point in list(getattr(obj, "Points", []) or []): - points.append(_vector(point)) + points.append(_placement_mult_vec(placement, _vector(point))) shape = getattr(obj, "Shape", None) if shape is not None and _is_route_path_source_object(obj): - for edge in list(getattr(shape, "Edges", []) or []): - points.extend(_edge_points(edge)) - if not points: + wires = list(getattr(shape, "Wires", []) or []) + if wires: + if points: + runs.append(points) + points = [] + for wire in wires: + runs.append(_points_with_placement(_wire_points(wire), placement)) + else: + for edge in list(getattr(shape, "Edges", []) or []): + points.extend(_points_with_placement(_edge_points(edge), placement)) + if not runs and not points: center = _shape_center(shape) if center is not None: - points.append(center) + points.append(_placement_mult_vec(placement, center)) - normalized = [] - for point in points: - if not normalized or _distance(normalized[-1], point) > DEFAULT_NODE_TOLERANCE: - normalized.append(point) - return normalized + if points: + runs.append(points) + + normalized_runs = [] + for run in runs: + normalized = _normalize_point_run(run) + if len(normalized) >= 2: + normalized_runs.append(normalized) + return normalized_runs + + +def _points_from_selection_item(selection_item): + points = [] + for run in _point_runs_from_selection_item(selection_item): + points.extend(run) + return _normalize_point_run(points) def _support_face_from_selection(selection_ex): @@ -1452,22 +1709,28 @@ def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUT for index, item in enumerate(selection_ex or [], start=1): if support_face is not None and _selection_item_is_only_support_face(item): continue - points = _points_from_selection_item(item) - if len(points) < 2: - continue + point_runs = _point_runs_from_selection_item(item) if support_face is not None: # 如果同时选中了支撑面和草图/线段,先把草图点投影到支撑面的平面上。 # Draft 自身只记录工作平面坐标,不会自动吸附到柜板面。 - points = _project_points_to_face(points, support_face) - created.append( - create_route_carrier( - doc, - points, - label="QET Route Carrier {0}".format(index), - project_uuid=project_uuid, - kind=kind, + point_runs = [_project_points_to_face(points, support_face) for points in point_runs] + point_runs = [_normalize_point_run(points) for points in point_runs] + point_runs = [points for points in point_runs if len(points) >= 2] + if not point_runs: + continue + for run_index, points in enumerate(point_runs, start=1): + label = "QET Route Carrier {0}".format(index) + if len(point_runs) > 1: + label = "{0} {1}".format(label, run_index) + created.append( + create_route_carrier( + doc, + points, + label=label, + project_uuid=project_uuid, + kind=kind, + ) ) - ) return created @@ -1485,7 +1748,7 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") if id(source) in seen_sources: continue seen_sources.add(id(source)) - if is_route_carrier(source): + if is_route_carrier(source) or is_routing_boundary(source): continue if ( _is_wire_duct_candidate(source) @@ -1493,11 +1756,13 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") or _is_wiring_cut_out_candidate(source) ): continue - points = _points_from_selection_item(item) - if len(points) < 2: - continue + point_runs = _point_runs_from_selection_item(item) if support_face is not None: - points = _project_points_to_face(points, support_face) + point_runs = [_project_points_to_face(points, support_face) for points in point_runs] + point_runs = [_normalize_point_run(points) for points in point_runs] + point_runs = [points for points in point_runs if len(points) >= 2] + if not point_runs: + continue label = "QET User Route Path {0}".format(index) capacity = 1 @@ -1506,109 +1771,429 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") getattr(source, "Label", "") or getattr(source, "Name", "") or index ) capacity = _route_carrier_capacity_value(source, default=1) - live_carrier = _live_source_carrier(doc, source) - if live_carrier is not None: - if _update_route_carrier( - live_carrier, - points, - project_uuid=project_uuid, - kind=ROUTE_CARRIER_KIND_USER_PATH, - capacity=capacity, - ): - _mark_user_path_source(source, live_carrier) + live_carriers = _live_source_carriers(doc, source) + if live_carriers: + refreshed = [] + for run_index, points in enumerate(point_runs, start=1): + if run_index <= len(live_carriers): + carrier = live_carriers[run_index - 1] + if _update_route_carrier( + carrier, + points, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, + ): + refreshed.append(carrier) + continue + run_label = label if len(point_runs) == 1 else "{0} {1}".format(label, run_index) + refreshed.append( + create_route_carrier( + doc, + points, + label=run_label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, + ) + ) + if len(live_carriers) > len(point_runs): + _remove_route_carriers(doc, live_carriers[len(point_runs) :]) + _mark_user_path_source_carriers(source, refreshed) + created.extend(refreshed) continue - carrier = create_route_carrier( - doc, - points, - label=label, - project_uuid=project_uuid, - kind=ROUTE_CARRIER_KIND_USER_PATH, - capacity=capacity, - ) + new_carriers = [] + for run_index, points in enumerate(point_runs, start=1): + run_label = label if len(point_runs) == 1 else "{0} {1}".format(label, run_index) + carrier = create_route_carrier( + doc, + points, + label=run_label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, + ) + new_carriers.append(carrier) + created.append(carrier) if source is not None: - _mark_user_path_source(source, carrier) - created.append(carrier) + _mark_user_path_source_carriers(source, new_carriers) return created -def _wire_duct_centerline_spec_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): - extents = { - axis: _bbox_extent(bbox, axis) - for axis in ("x", "y", "z") - } - main_axis = max(extents, key=extents.get) - sorted_extents = sorted(extents.values(), reverse=True) - if sorted_extents[0] <= DEFAULT_NODE_TOLERANCE: - return {"centerline": [], "open_ends": []} - if len(sorted_extents) > 1 and sorted_extents[1] > DEFAULT_NODE_TOLERANCE: - if sorted_extents[0] / sorted_extents[1] < float(min_aspect or 1.0): - return {"centerline": [], "open_ends": []} +def _nearest_points_between_route_point_runs(left_points, right_points): + left_points = _normalized_route_points(left_points) + right_points = _normalized_route_points(right_points) + if len(left_points) < 2 or len(right_points) < 2: + return None - low, high = _bbox_axis_range(bbox, main_axis) - center = _bbox_center(bbox) - usable_margin = max(float(margin or 0.0), 0.0) - if abs(high - low) <= usable_margin * 2.0: - usable_margin = 0.0 + best = None - start = _set_axis(center, main_axis, low + usable_margin) - end = _set_axis(center, main_axis, high - usable_margin) - if _distance(start, end) <= DEFAULT_NODE_TOLERANCE: - return {"centerline": [], "open_ends": []} + def remember(left_point, right_point): + nonlocal best + distance = _distance(left_point, right_point) + if best is None or distance < best[0]: + best = (distance, left_point, right_point) - cross_axes = sorted( - [axis for axis in ("x", "y", "z") if axis != main_axis], - key=lambda axis: _bbox_extent(bbox, axis), - reverse=True, - ) - open_ends = [] - if cross_axes: - cross_axis = cross_axes[0] - cross_extent = _bbox_extent(bbox, cross_axis) - half_length = max( - min(cross_extent * 0.5, float(margin or DEFAULT_WIRE_DUCT_MARGIN)), - min(cross_extent * 0.5, DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH * 0.5), - ) - if half_length > DEFAULT_NODE_TOLERANCE: - for endpoint in (start, end): - open_ends.append( - [ - _set_axis(endpoint, cross_axis, _axis_value(center, cross_axis) - half_length), - _set_axis(endpoint, cross_axis, _axis_value(center, cross_axis) + half_length), - ] - ) + for point in left_points: + for index in range(len(right_points) - 1): + projected = _closest_point_on_segment(point, right_points[index], right_points[index + 1]) + remember(point, projected) - return { - "centerline": [start, end], - "open_ends": open_ends, - "main_axis": main_axis, - } + for point in right_points: + for index in range(len(left_points) - 1): + projected = _closest_point_on_segment(point, left_points[index], left_points[index + 1]) + remember(projected, point) + return best -def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): - return _wire_duct_centerline_spec_from_bbox( - bbox, - margin=margin, - min_aspect=min_aspect, - ).get("centerline", []) +def create_user_path_bridge_from_selection(doc, selection_ex, project_uuid=""): + """Create a short user-controlled bridge between two selected route carriers. -def _sync_wire_duct_source_carriers( - doc, - source, - spec, - project_uuid="", - capacity=1, - end_margin=DEFAULT_WIRE_DUCT_MARGIN, -): - carriers = _live_source_carriers(doc, source) - if not carriers: - return False + 这里刻意只连接用户选中的路径,不做全局远距离自动桥接; + 否则真实机柜里相互无关的线槽、布线面可能会被误接成一个错误网络。 + """ + carriers = _selected_route_carriers_for_constraint(doc, selection_ex) + if len(carriers) < 2: + return [] - desired = [ - (spec.get("centerline", []), ROUTE_CARRIER_KIND_WIRE_DUCT), - ] - desired.extend( + left = carriers[0] + right = carriers[1] + left_points = _carrier_points(left) + right_points = _carrier_points(right) + nearest = _nearest_points_between_route_point_runs(left_points, right_points) + if nearest is None: + return [] + + _distance_mm, left_point, right_point = nearest + if _distance(left_point, right_point) <= DEFAULT_NODE_TOLERANCE: + return [] + + left_label = getattr(left, "Label", "") or getattr(left, "Name", "") or "Path A" + right_label = getattr(right, "Label", "") or getattr(right, "Name", "") or "Path B" + bridge = create_route_carrier( + doc, + [left_point, right_point], + label="QET User Bridge {0} -> {1}".format(left_label, right_label), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=min( + _route_carrier_capacity_value(left, default=1), + _route_carrier_capacity_value(right, default=1), + ), + ) + return [bridge] + + +def _route_carriers_for_bridge_object(doc, source): + if source is None: + return [] + if is_route_carrier(source): + return [source] + carriers = [ + carrier + for carrier in _live_source_carriers(doc, source) + if carrier is not None and is_route_carrier(carrier) + ] + if carriers: + return carriers + source_name = (getattr(source, "Name", "") or "").strip() + source_label = (getattr(source, "Label", "") or "").strip() + source_route_label = (getattr(source, "QetRouteSourceLabel", "") or "").strip() + seen = set() + for candidate in collect_route_carriers(doc): + if candidate is None or not is_route_carrier(candidate): + continue + if ( + source_name + and (getattr(candidate, "QetRouteSourceName", "") or "").strip() == source_name + ) or ( + source_label + and (getattr(candidate, "QetRouteSourceLabel", "") or "").strip() == source_label + ) or ( + source_route_label + and (getattr(candidate, "QetRouteSourceLabel", "") or "").strip() == source_route_label + ): + identity = id(candidate) + if identity in seen: + continue + seen.add(identity) + carriers.append(candidate) + 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.""" + left_carriers = _route_carriers_for_bridge_object(doc, left_source) + right_carriers = _route_carriers_for_bridge_object(doc, right_source) + best = None + for left in left_carriers: + left_points = _carrier_points(left) + for right in right_carriers: + if left is right: + continue + right_points = _carrier_points(right) + nearest = _nearest_points_between_route_point_runs(left_points, right_points) + 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: + return [] + + distance_mm, left, right, left_point, right_point = best + if _distance(left_point, right_point) <= DEFAULT_NODE_TOLERANCE: + return [] + if _route_bridge_already_exists(doc, left_point, right_point): + return [] + + left_label = ( + getattr(left_source, "QetRouteSourceLabel", "") + or getattr(left_source, "Label", "") + or getattr(left, "QetRouteSourceLabel", "") + or getattr(left, "Label", "") + or getattr(left, "Name", "") + or "Path A" + ) + right_label = ( + getattr(right_source, "QetRouteSourceLabel", "") + or getattr(right_source, "Label", "") + or getattr(right, "QetRouteSourceLabel", "") + or getattr(right, "Label", "") + or getattr(right, "Name", "") + or "Path B" + ) + bridge = create_route_carrier( + doc, + [left_point, right_point], + label="QET User Bridge {0} -> {1}".format(left_label, right_label), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=min( + _route_carrier_capacity_value(left, default=1), + _route_carrier_capacity_value(right, default=1), + ), + ) + # 缺主路径绕行桥接需要保留来源,便于用户后续在 FreeCAD 树中复核是哪两个区域被自动补路。 + TerminalObjects.ensure_string_property( + bridge, + "QetRouteBridgeKind", + PROPERTY_GROUP, + "QET route bridge kind", + "MainPathDetourBridge", + ) + TerminalObjects.ensure_string_property( + bridge, + "QetRouteBridgePairLabel", + PROPERTY_GROUP, + "Human readable source pair for this generated bridge", + "{0} -> {1}".format(left_label, right_label), + ) + TerminalObjects.ensure_string_property( + bridge, + "QetRouteBridgeLeftSourceName", + PROPERTY_GROUP, + "Left/source object name for this generated bridge", + getattr(left_source, "Name", "") or getattr(left, "QetRouteSourceName", "") or getattr(left, "Name", ""), + ) + TerminalObjects.ensure_string_property( + bridge, + "QetRouteBridgeRightSourceName", + PROPERTY_GROUP, + "Right/source object name for this generated bridge", + getattr(right_source, "Name", "") or getattr(right, "QetRouteSourceName", "") or getattr(right, "Name", ""), + ) + TerminalObjects.ensure_string_property( + bridge, + "QetRouteBridgeLeftSourceLabel", + PROPERTY_GROUP, + "Left/source object label for this generated bridge", + left_label, + ) + TerminalObjects.ensure_string_property( + bridge, + "QetRouteBridgeRightSourceLabel", + PROPERTY_GROUP, + "Right/source object label for this generated bridge", + right_label, + ) + return [bridge] + + +def _route_bridge_already_exists(doc, left_point, right_point): + for carrier in collect_route_carriers(doc): + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() + if kind != ROUTE_CARRIER_KIND_USER_PATH: + continue + points = _normalized_route_points(_carrier_points(carrier)) + if len(points) != 2: + continue + if ( + _distance(points[0], left_point) <= DEFAULT_NODE_TOLERANCE + and _distance(points[1], right_point) <= DEFAULT_NODE_TOLERANCE + ) or ( + _distance(points[0], right_point) <= DEFAULT_NODE_TOLERANCE + and _distance(points[1], left_point) <= DEFAULT_NODE_TOLERANCE + ): + return True + return False + + +def create_user_path_bridges_from_diagnostic_suggestions(doc, diagnostic, project_uuid=""): + """Create UserPath bridges from explicit path-network diagnostic suggestions.""" + report = { + "suggestions": 0, + "created": [], + "duplicates": 0, + "stale_suggestions": 0, + } + if doc is None or not isinstance(diagnostic, dict): + return report + + for item in diagnostic.get("wire_ducts_without_terminal_access", []) or []: + if not isinstance(item, dict): + continue + suggestion = item.get("bridge_suggestion", {}) + if not isinstance(suggestion, dict) or not suggestion: + continue + report["suggestions"] += 1 + from_carrier_payload = suggestion.get("from_carrier", {}) + to_carrier_payload = suggestion.get("to_carrier", {}) + if not isinstance(from_carrier_payload, dict) or not isinstance(to_carrier_payload, dict): + report["stale_suggestions"] += 1 + continue + from_carrier = _document_object_by_name(doc, from_carrier_payload.get("name", "")) + to_carrier = _document_object_by_name(doc, to_carrier_payload.get("name", "")) + if not is_route_carrier(from_carrier) or not is_route_carrier(to_carrier): + report["stale_suggestions"] += 1 + continue + try: + from_point = _vector(suggestion.get("from_point", {})) + to_point = _vector(suggestion.get("to_point", {})) + except Exception: + report["stale_suggestions"] += 1 + continue + if _distance(from_point, to_point) <= DEFAULT_NODE_TOLERANCE: + report["duplicates"] += 1 + continue + if _route_bridge_already_exists(doc, from_point, to_point): + report["duplicates"] += 1 + continue + label = "QET User Bridge {0} -> {1}".format( + from_carrier_payload.get("label") or from_carrier_payload.get("name") or "Path A", + to_carrier_payload.get("label") or to_carrier_payload.get("name") or "Path B", + ) + bridge = create_route_carrier( + doc, + [from_point, to_point], + label=label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=min( + _route_carrier_capacity_value(from_carrier, default=1), + _route_carrier_capacity_value(to_carrier, default=1), + ), + ) + report["created"].append(bridge) + return report + + +def mark_cabinet_interior_boundaries_from_selection(selection_ex): + """Mark selected FreeCAD objects as cabinet interior routing boundaries.""" + marked = [] + seen_sources = set() + for item in selection_ex or []: + source = getattr(item, "Object", None) + if source is None or id(source) in seen_sources: + continue + seen_sources.add(id(source)) + if is_route_carrier(source) or not _object_has_bbox(source): + continue + # 这里只写 FreeCAD 文档对象语义,后续布线按包围盒判断是否跑出柜内区域。 + _set_cabinet_interior_boundary_semantics(source) + marked.append(source) + return marked + + +def _wire_duct_centerline_spec_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): + extents = { + axis: _bbox_extent(bbox, axis) + for axis in ("x", "y", "z") + } + main_axis = max(extents, key=extents.get) + sorted_extents = sorted(extents.values(), reverse=True) + if sorted_extents[0] <= DEFAULT_NODE_TOLERANCE: + return {"centerline": [], "open_ends": []} + if len(sorted_extents) > 1 and sorted_extents[1] > DEFAULT_NODE_TOLERANCE: + if sorted_extents[0] / sorted_extents[1] < float(min_aspect or 1.0): + return {"centerline": [], "open_ends": []} + + low, high = _bbox_axis_range(bbox, main_axis) + center = _bbox_center(bbox) + usable_margin = max(float(margin or 0.0), 0.0) + if abs(high - low) <= usable_margin * 2.0: + usable_margin = 0.0 + + start = _set_axis(center, main_axis, low + usable_margin) + end = _set_axis(center, main_axis, high - usable_margin) + if _distance(start, end) <= DEFAULT_NODE_TOLERANCE: + return {"centerline": [], "open_ends": []} + + cross_axes = sorted( + [axis for axis in ("x", "y", "z") if axis != main_axis], + key=lambda axis: _bbox_extent(bbox, axis), + reverse=True, + ) + open_ends = [] + if cross_axes: + cross_axis = cross_axes[0] + cross_extent = _bbox_extent(bbox, cross_axis) + half_length = max( + min(cross_extent * 0.5, float(margin or DEFAULT_WIRE_DUCT_MARGIN)), + min(cross_extent * 0.5, DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH * 0.5), + ) + if half_length > DEFAULT_NODE_TOLERANCE: + for endpoint in (start, end): + open_ends.append( + [ + _set_axis(endpoint, cross_axis, _axis_value(center, cross_axis) - half_length), + _set_axis(endpoint, cross_axis, _axis_value(center, cross_axis) + half_length), + ] + ) + + return { + "centerline": [start, end], + "open_ends": open_ends, + "main_axis": main_axis, + } + + +def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_aspect=1.5): + return _wire_duct_centerline_spec_from_bbox( + bbox, + margin=margin, + min_aspect=min_aspect, + ).get("centerline", []) + + +def _sync_wire_duct_source_carriers( + doc, + source, + spec, + project_uuid="", + capacity=1, + end_margin=DEFAULT_WIRE_DUCT_MARGIN, +): + carriers = _live_source_carriers(doc, source) + if not carriers: + return False + + desired = [ + (spec.get("centerline", []), ROUTE_CARRIER_KIND_WIRE_DUCT), + ] + desired.extend( (points, ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END) for points in (spec.get("open_ends", []) or []) ) @@ -1715,7 +2300,7 @@ def _source_kind_value(source): return (getattr(source, "QetRoutingSourceKind", "") or "").strip() -def _set_route_carrier_source_metadata(carrier, source, source_kind=""): +def _set_route_carrier_source_metadata(carrier, source, source_kind="", source_path_index=None): if carrier is None or source is None: return source_name = (getattr(source, "Name", "") or "").strip() @@ -1743,6 +2328,32 @@ def _set_route_carrier_source_metadata(carrier, source, source_kind=""): "Routing source kind that generated this route carrier", kind, ) + if source_path_index is not None: + TerminalObjects.ensure_string_property( + carrier, + "QetRouteSourcePathIndex", + PROPERTY_GROUP, + "1-based path index generated from the same routing source", + str(source_path_index), + ) + elif "QetRouteSourcePathIndex" in getattr(carrier, "PropertiesList", []) or getattr( + carrier, "QetRouteSourcePathIndex", "" + ): + TerminalObjects.ensure_string_property( + carrier, + "QetRouteSourcePathIndex", + PROPERTY_GROUP, + "1-based path index generated from the same routing source", + "", + ) + constraint_mode = (getattr(source, "QetRouteConstraintMode", "") or "").strip() + TerminalObjects.ensure_string_property( + carrier, + "QetRouteConstraintMode", + PROPERTY_GROUP, + "Route constraint mode for automatic routing", + constraint_mode, + ) def _remember_source_carriers(source, carriers): @@ -1753,8 +2364,19 @@ def _remember_source_carriers(source, carriers): ] if live_names: source_kind = _source_kind_value(source) - for carrier in carriers or []: - _set_route_carrier_source_metadata(carrier, source, source_kind=source_kind) + for index, carrier in enumerate(carriers or [], start=1): + # 多 Wire 草图会生成多条 UserPath,序号用于诊断和路径样例回溯。 + source_path_index = ( + index + if source_kind == ROUTE_CARRIER_KIND_USER_PATH and len(live_names) > 1 + else None + ) + _set_route_carrier_source_metadata( + carrier, + source, + source_kind=source_kind, + source_path_index=source_path_index, + ) TerminalObjects.ensure_string_property( source, "QetRouteCarrierNamesJson", @@ -1833,6 +2455,24 @@ def _mark_user_path_source(source, carrier): pass +def _mark_user_path_source_carriers(source, carriers): + carriers = [carrier for carrier in (carriers or []) if carrier is not None] + if source is None or not carriers: + return + try: + _set_user_path_source_semantics(source) + TerminalObjects.ensure_string_property( + source, + "QetRouteCarrierName", + PROPERTY_GROUP, + "Generated route carrier for this source", + getattr(carriers[0], "Name", ""), + ) + _remember_source_carriers(source, carriers) + except Exception: + pass + + def _mark_terminal_access_source(source, carrier): if source is None or carrier is None: return @@ -1973,6 +2613,67 @@ def detect_wiring_cut_out_sources(doc): return sources +def detect_user_path_sources(doc): + """Return sketch/Draft-like route path source objects that can become UserPath carriers.""" + sources = [] + seen = set() + for obj in list(getattr(doc, "Objects", []) or []): + if id(obj) in seen: + continue + seen.add(id(obj)) + if _is_route_path_source_object(obj): + sources.append(obj) + return sources + + +def _source_sample(obj): + return { + "name": getattr(obj, "Name", ""), + "label": getattr(obj, "Label", ""), + "type_id": getattr(obj, "TypeId", ""), + "source_kind": (getattr(obj, "QetRoutingSourceKind", "") or "").strip(), + } + + +def routing_source_summary(doc): + """Summarize routing sources without creating or modifying carriers.""" + wire_duct_sources = detect_wire_duct_sources(doc) + support_surface_sources = detect_support_surface_sources(doc) + wiring_cut_out_sources = detect_wiring_cut_out_sources(doc) + user_path_sources = detect_user_path_sources(doc) + carriers = collect_route_carriers(doc) + candidate_objects = [] + seen = set() + for obj in ( + list(wire_duct_sources) + + list(support_surface_sources) + + list(wiring_cut_out_sources) + + list(user_path_sources) + ): + if id(obj) in seen: + continue + seen.add(id(obj)) + candidate_objects.append(obj) + marked_source_counts = {} + for obj in list(getattr(doc, "Objects", []) or []): + if obj is None or is_route_carrier(obj): + continue + source_kind = _source_kind_value(obj) + if not source_kind: + continue + marked_source_counts[source_kind] = marked_source_counts.get(source_kind, 0) + 1 + return { + "wire_duct_sources": len(wire_duct_sources), + "support_surface_sources": len(support_surface_sources), + "wiring_cut_out_sources": len(wiring_cut_out_sources), + "user_path_sources": len(user_path_sources), + "candidate_sources": len(candidate_objects), + "route_carriers": len(carriers), + "marked_source_counts": marked_source_counts, + "candidate_samples": [_source_sample(obj) for obj in candidate_objects[:8]], + } + + def prepare_layout_space_sources_from_document(doc, project_uuid=""): """Normalize the current FreeCAD document as an EPLAN-style layout space. @@ -2143,6 +2844,7 @@ def create_surface_carriers_from_document( offset=offset, margin=margin, ) + capacity = _route_carrier_capacity_value(source, default=1) live_carriers = _live_source_carriers(doc, source) if live_carriers: updated = [] @@ -2152,6 +2854,7 @@ def create_surface_carriers_from_document( points, project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + capacity=capacity, ): updated.append(carrier) source_created = [] @@ -2165,6 +2868,7 @@ def create_surface_carriers_from_document( label="QET Auto Support Surface Route {0} {1}".format(label, index), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + capacity=capacity, ) source_created.append(carrier) created.append(carrier) @@ -2194,6 +2898,7 @@ def create_surface_carriers_from_document( label="QET Auto Support Surface Route {0} {1}".format(label, index), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + capacity=capacity, ) source_created.append(carrier) created.append(carrier) @@ -2251,6 +2956,17 @@ def _json_route_point(item): return None +def _local_route_point_items(parsed): + if isinstance(parsed, list): + return parsed + if isinstance(parsed, dict): + for key in ("points", "route_points", "local_points"): + value = parsed.get(key) + if isinstance(value, list): + return value + return None + + def _terminal_local_route_points(terminal): for property_name in ("QetTerminalLocalRoutePointsJson", "QetLocalRoutePointsJson"): raw = (getattr(terminal, property_name, "") or "").strip() @@ -2260,7 +2976,10 @@ def _terminal_local_route_points(terminal): parsed = json.loads(raw) except Exception: continue - points = [_json_route_point(item) for item in parsed if item is not None] + point_items = _local_route_point_items(parsed) + if point_items is None: + continue + points = [_json_route_point(item) for item in point_items if item is not None] points = [point for point in points if point is not None] if points: return points @@ -2287,17 +3006,18 @@ def _terminal_local_route_issue(terminal): } ) continue - if not isinstance(parsed, list): + point_items = _local_route_point_items(parsed) + if point_items is None: invalid_samples.append( { "property_name": property_name, "reason": "not_array", - "message": "Local route points JSON must be an array.", + "message": "Local route points JSON must be an array or an object with a points array.", "raw_sample": raw[:160], } ) continue - points = [_json_route_point(item) for item in parsed if item is not None] + points = [_json_route_point(item) for item in point_items if item is not None] valid_points = [point for point in points if point is not None] if len(_normalized_route_points(valid_points)) >= 2: return None @@ -2340,21 +3060,6 @@ def _terminal_parent_chain(terminal): return chain -def _placement_mult_vec(placement, point): - if placement is None: - return point - try: - transformed = placement.multVec(point) - if transformed is not None: - return _vector(transformed) - except Exception: - pass - base = getattr(placement, "Base", None) - if base is not None: - return App.Vector(point.x + base.x, point.y + base.y, point.z + base.z) - return point - - def _terminal_local_point_to_global(terminal, local_point): try: if hasattr(terminal, "getGlobalPlacement"): @@ -2369,7 +3074,98 @@ def _terminal_local_point_to_global(terminal, local_point): return point -def terminal_access_path_points(terminal, exit_length=20.0): +def _document_point_to_terminal_local(terminal, point): + point = _vector(point) + try: + if hasattr(terminal, "getGlobalPlacement"): + placement = terminal.getGlobalPlacement() + inverse = placement.inverse() + transformed = inverse.multVec(point) + if transformed is not None: + return _vector(transformed) + except Exception: + pass + + origin = _vector(TerminalObjects.terminal_origin(terminal)) + return App.Vector( + float(point.x) - float(origin.x), + float(point.y) - float(origin.y), + float(point.z) - float(origin.z), + ) + + +def _local_route_point_payload(point): + point = _vector(point) + return [float(point.x), float(point.y), float(point.z)] + + +def set_terminal_local_route_points(terminal, document_points): + """Store a field-authored local exit path 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("端子局部出线路径至少需要两个有效路径点。") + + local_points = [_document_point_to_terminal_local(terminal, point) for point in points] + local_points = _normalized_route_points(local_points) + if len(local_points) < 2: + raise RoutingNetworkError("端子局部出线路径转换后少于两个有效路径点。") + payload = [_local_route_point_payload(point) for point in local_points] + TerminalObjects.ensure_string_property( + terminal, + "QetTerminalLocalRoutePointsJson", + PROPERTY_GROUP, + "端子到柜内主路径网络前的局部出线路径点", + json.dumps(payload, ensure_ascii=False), + ) + try: + terminal.Document.recompute() + except Exception: + pass + return { + "terminal": terminal, + "point_count": len(payload), + "property_name": "QetTerminalLocalRoutePointsJson", + "points": payload, + } + + +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 + support_face = _support_face_from_selection(selection_ex) + route_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 + if support_face is not None and _selection_item_is_only_support_face(item): + continue + + point_runs = _point_runs_from_selection_item(item) + if support_face is not None: + # 允许用户先选安装板/面,再选草图线,把局部出线贴到该面附近。 + point_runs = [_project_points_to_face(points, support_face) for points in point_runs] + for points in point_runs: + normalized = _normalize_point_run(points) + if len(normalized) >= 2: + route_runs.append(normalized) + + if terminal is None: + raise RoutingNetworkError("请同时选中一个可布线端子和一条草图/Draft 局部出线路径。") + if not route_runs: + raise RoutingNetworkError("请选择至少包含两个点的草图、Draft 线、边或路径对象。") + if len(route_runs) > 1: + raise RoutingNetworkError("端子局部出线路径一次只支持一条连续路径,请只选择一条草图线或一个连续 Wire。") + return set_terminal_local_route_points(terminal, route_runs[0]) + + +def terminal_access_path_points(terminal, exit_length=20.0): """Return terminal-to-network access points, honoring optional local route metadata.""" origin = _vector(TerminalObjects.terminal_origin(terminal)) local_points = _terminal_local_route_points(terminal) @@ -2479,6 +3275,9 @@ def rank_connection_point_candidates(network, candidates): if max_primary_segments > 0 and component_primary_segments <= 0: score += DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY carrier_kind = (getattr(candidate.get("carrier"), "QetRouteCarrierKind", "") or "").strip() + if max_primary_segments > 0 and not _is_primary_route_carrier(candidate.get("carrier")): + # 同一网络组件里也优先接线槽/UserPath/过线孔;RoutingRange 只是兜底布线面。 + score += DEFAULT_TERMINAL_ACCESS_FALLBACK_CARRIER_PENALTY if max_primary_segments > 0 and carrier_kind == ROUTE_CARRIER_KIND_TERMINAL_ACCESS: # 入口候选也要优先真实主路径,避免导线贴到其它端子的局部接入线上起步。 score += DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY @@ -2492,6 +3291,88 @@ def rank_connection_point_candidates(network, candidates): return ranked +def _component_index_by_node(network): + nodes = network.get("nodes", {}) if isinstance(network, dict) else {} + edges = network.get("edges", {}) if isinstance(network, dict) else {} + component_by_node = {} + seen = set() + component_index = 0 + for start_key in nodes.keys(): + if start_key in seen: + continue + stack = [start_key] + seen.add(start_key) + while stack: + key = stack.pop() + component_by_node[key] = component_index + for next_key, _weight, _carrier in edges.get(key, []) or []: + if next_key not in seen: + seen.add(next_key) + stack.append(next_key) + component_index += 1 + return component_by_node + + +def _candidate_component_index(candidate, component_by_node): + for key_name in ("projected_key", "key", "next_key"): + key = candidate.get(key_name) + if key in component_by_node: + return component_by_node[key] + return None + + +def _candidate_identity(candidate): + carrier = candidate.get("carrier") + return ( + candidate.get("projected_key"), + candidate.get("key"), + candidate.get("next_key"), + id(carrier) if carrier is not None else None, + ) + + +def select_diverse_connection_point_candidates(network, candidates, limit=8): + """Select ranked entry candidates while keeping alternate components visible.""" + max_items = max(int(limit or 0), 0) + ranked = [] + seen_identities = set() + for candidate in rank_connection_point_candidates(network, candidates): + identity = _candidate_identity(candidate) + if identity in seen_identities: + continue + seen_identities.add(identity) + ranked.append(candidate) + if max_items <= 0 or len(ranked) <= max_items: + return ranked + + component_by_node = _component_index_by_node(network) + selected = [] + selected_identities = set() + selected_components = set() + deferred = [] + for candidate in ranked: + identity = _candidate_identity(candidate) + component_index = _candidate_component_index(candidate, component_by_node) + if component_index is None or component_index in selected_components: + deferred.append(candidate) + continue + selected.append(candidate) + selected_identities.add(identity) + selected_components.add(component_index) + if len(selected) >= max_items: + return selected + + for candidate in deferred: + identity = _candidate_identity(candidate) + if identity in selected_identities: + continue + selected.append(candidate) + selected_identities.add(identity) + if len(selected) >= max_items: + break + return selected + + def _terminal_access_target_candidate(network, exit_point, max_distance): candidates = connection_point_candidates( network, @@ -2736,6 +3617,8 @@ def create_surface_carriers_from_selection( created = [] for item in selection_ex or []: item_created = [] + selection_source = getattr(item, "Object", None) + capacity = _route_carrier_capacity_value(selection_source, default=1) for sub_object in list(getattr(item, "SubObjects", []) or []): shape_type = (getattr(sub_object, "ShapeType", "") or "").lower() if shape_type != "face": @@ -2755,6 +3638,7 @@ def create_surface_carriers_from_selection( label="QET Surface Route {0}".format(index), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + capacity=capacity, ) item_created.append(carrier) created.append(carrier) @@ -2776,6 +3660,7 @@ def create_surface_carriers_from_selection( offset=offset, margin=margin, ) + capacity = _route_carrier_capacity_value(obj, default=1) source_created = [] label = getattr(obj, "Label", "") or getattr(obj, "Name", "") or "Support Surface" for index, points in enumerate(grids, start=1): @@ -2787,6 +3672,7 @@ def create_surface_carriers_from_selection( label="QET Surface Route {0} {1}".format(label, index), project_uuid=project_uuid, kind=ROUTE_CARRIER_KIND_ROUTING_RANGE, + capacity=capacity, ) source_created.append(carrier) created.append(carrier) @@ -2809,6 +3695,7 @@ def build_route_graph( doc, tolerance=DEFAULT_NODE_TOLERANCE, blocked_bboxes=None, + allowed_bboxes=None, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): """Build an undirected graph from every enabled route carrier.""" @@ -2817,8 +3704,10 @@ def build_route_graph( carriers = collect_route_carriers(doc) segment_count = 0 blocked_segment_count = 0 + boundary_filtered_segment_count = 0 bridged_segment_count = 0 blocked_bboxes = list(blocked_bboxes or []) + allowed_bboxes = list(allowed_bboxes or []) segments = [] bridgeable_endpoint_nodes = [] projection_bridge_candidates = [] @@ -2885,12 +3774,13 @@ def build_route_graph( continue projected = _closest_point_on_segment(endpoint, right["start"], right["end"]) distance = _distance(endpoint, projected) - if distance <= tolerance or distance > adjoining_limit: + if distance > adjoining_limit: continue right["points"].append(projected) - projection_bridge_candidates.append( - (endpoint, projected, left_carrier, right_carrier) - ) + if distance > tolerance: + projection_bridge_candidates.append( + (endpoint, projected, left_carrier, right_carrier) + ) for segment in segments: ordered = _sorted_segment_points( @@ -2914,6 +3804,11 @@ def build_route_graph( current_point = nodes[current_key] weight = _distance(previous_point, current_point) if weight > tolerance: + if allowed_bboxes and not _segment_inside_any_bbox(previous_point, current_point, allowed_bboxes): + boundary_filtered_segment_count += 1 + previous_key = current_key + previous_point = current_point + continue if _segment_hits_blocked_bbox(previous_point, current_point, blocked_bboxes): blocked_segment_count += 1 previous_key = current_key @@ -2928,7 +3823,7 @@ def build_route_graph( bridged_pairs = set() def add_bridge_edge(left_key, left_point, left_carrier, right_key, right_point, right_carrier): - nonlocal blocked_segment_count, bridged_segment_count, segment_count + nonlocal blocked_segment_count, boundary_filtered_segment_count, bridged_segment_count, segment_count if left_key == right_key or left_carrier is right_carrier: return pair = tuple(sorted((left_key, right_key))) @@ -2939,6 +3834,9 @@ def build_route_graph( return if any(next_key == right_key for next_key, _weight, _carrier in edges.get(left_key, [])): return + if allowed_bboxes and not _segment_inside_any_bbox(left_point, right_point, allowed_bboxes): + boundary_filtered_segment_count += 1 + return if _segment_hits_blocked_bbox(left_point, right_point, blocked_bboxes): blocked_segment_count += 1 return @@ -2975,6 +3873,8 @@ def build_route_graph( "segment_count": segment_count, "bridged_segment_count": bridged_segment_count, "blocked_segment_count": blocked_segment_count, + "boundary_filtered": bool(allowed_bboxes), + "boundary_filtered_segment_count": boundary_filtered_segment_count, "tolerance": tolerance, } @@ -3182,6 +4082,7 @@ def _carrier_track_payload(carrier): ("source_name", "QetRouteSourceName"), ("source_label", "QetRouteSourceLabel"), ("source_kind", "QetRouteSourceKind"), + ("source_path_index", "QetRouteSourcePathIndex"), ) for payload_key, property_name in source_fields: value = (getattr(carrier, property_name, "") or "").strip() @@ -3211,6 +4112,78 @@ def _carrier_capacity(carrier): return 1 +def _normalized_text_set(values): + return { + str(value or "").strip() + for value in (values or []) + if str(value or "").strip() + } + + +def _carrier_forbidden( + carrier, + forbidden_carrier_names=None, + forbidden_carrier_labels=None, + forbidden_carrier_source_names=None, + forbidden_carrier_source_labels=None, + forbidden_carrier_kinds=None, +): + if carrier is None: + return False + names = _normalized_text_set(forbidden_carrier_names) + labels = _normalized_text_set(forbidden_carrier_labels) + source_names = _normalized_text_set(forbidden_carrier_source_names) + source_labels = _normalized_text_set(forbidden_carrier_source_labels) + kinds = _normalized_text_set(forbidden_carrier_kinds) + if names and (getattr(carrier, "Name", "") or "").strip() in names: + return True + if labels and (getattr(carrier, "Label", "") or "").strip() in labels: + return True + if source_names and (getattr(carrier, "QetRouteSourceName", "") or "").strip() in source_names: + return True + if source_labels and (getattr(carrier, "QetRouteSourceLabel", "") or "").strip() in source_labels: + return True + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + return bool(kinds and kind in kinds) + + +def _required_carrier_criteria( + required_carrier_names=None, + required_carrier_labels=None, + required_carrier_source_names=None, + required_carrier_source_labels=None, + required_carrier_kinds=None, +): + criteria = [] + for kind, values in ( + ("name", required_carrier_names), + ("label", required_carrier_labels), + ("source_name", required_carrier_source_names), + ("source_label", required_carrier_source_labels), + ("kind", required_carrier_kinds), + ): + for value in sorted(_normalized_text_set(values)): + criteria.append((kind, value)) + return criteria[:30] + + +def _carrier_required_mask(carrier, criteria): + if carrier is None or not criteria: + return 0 + values = { + "name": (getattr(carrier, "Name", "") or "").strip(), + "label": (getattr(carrier, "Label", "") or "").strip(), + "source_name": (getattr(carrier, "QetRouteSourceName", "") or "").strip(), + "source_label": (getattr(carrier, "QetRouteSourceLabel", "") or "").strip(), + "kind": (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND, + } + mask = 0 + for index, (kind, expected) in enumerate(criteria): + if values.get(kind, "") == expected: + mask |= 1 << index + return mask + + def shortest_path_with_carriers( network, start_key, @@ -3220,11 +4193,29 @@ def shortest_path_with_carriers( segment_usage_costs=None, segment_reuse_penalty=0.0, excluded_transit_carrier_kinds=None, + forbidden_carrier_names=None, + forbidden_carrier_labels=None, + forbidden_carrier_source_names=None, + forbidden_carrier_source_labels=None, + forbidden_carrier_kinds=None, + required_carrier_names=None, + required_carrier_labels=None, + required_carrier_source_names=None, + required_carrier_source_labels=None, + required_carrier_kinds=None, ): """Dijkstra search with a small extra cost when route direction changes.""" if start_key is None or end_key is None: return None - if start_key == end_key: + required_criteria = _required_carrier_criteria( + required_carrier_names=required_carrier_names, + required_carrier_labels=required_carrier_labels, + required_carrier_source_names=required_carrier_source_names, + required_carrier_source_labels=required_carrier_source_labels, + required_carrier_kinds=required_carrier_kinds, + ) + required_all_mask = (1 << len(required_criteria)) - 1 + if start_key == end_key and required_all_mask == 0: return { "path": [start_key], "segments": [], @@ -3242,17 +4233,17 @@ def shortest_path_with_carriers( } queue = [] counter = 0 - start_state = (start_key, None) + start_state = (start_key, None, 0) distances = {start_state: 0.0} previous = {} - heapq.heappush(queue, (0.0, counter, start_key, None)) + heapq.heappush(queue, (0.0, counter, start_key, None, 0)) while queue: - cost, _counter, key, previous_direction = heapq.heappop(queue) - state = (key, previous_direction) + cost, _counter, key, previous_direction, required_mask = heapq.heappop(queue) + state = (key, previous_direction, required_mask) if cost > distances.get(state, float("inf")): continue - if key == end_key: + if key == end_key and required_mask == required_all_mask: path = [key] segments = [] current_state = state @@ -3308,7 +4299,17 @@ def shortest_path_with_carriers( # TerminalAccess 是端子局部接入线,不能被其它导线当作柜内主路径或公共桥接段。 if carrier_kind in excluded_transit_kinds: continue + if _carrier_forbidden( + carrier, + forbidden_carrier_names=forbidden_carrier_names, + forbidden_carrier_labels=forbidden_carrier_labels, + forbidden_carrier_source_names=forbidden_carrier_source_names, + forbidden_carrier_source_labels=forbidden_carrier_source_labels, + forbidden_carrier_kinds=forbidden_carrier_kinds, + ): + continue direction = _direction_key(nodes[key], nodes[next_key]) + next_required_mask = required_mask | _carrier_required_mask(carrier, required_criteria) bend_cost = 0.0 if previous_direction is not None and direction != previous_direction: bend_cost = float(bend_penalty or 0.0) @@ -3318,7 +4319,7 @@ def shortest_path_with_carriers( capacity = float(_carrier_capacity(carrier)) excess_usage = max(usage_count - capacity + 1.0, 0.0) usage_cost = excess_usage * float(segment_reuse_penalty or 0.0) - next_state = (next_key, direction) + next_state = (next_key, direction, next_required_mask) next_cost = ( cost + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) @@ -3333,7 +4334,7 @@ def shortest_path_with_carriers( "weight": weight, } counter += 1 - heapq.heappush(queue, (next_cost, counter, next_key, direction)) + heapq.heappush(queue, (next_cost, counter, next_key, direction, next_required_mask)) return None @@ -3348,6 +4349,198 @@ def network_summary(doc, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERAN return _network_summary_from_graph(network) +def collect_route_constraint_options(doc): + """Collect global route constraints marked directly on route carrier objects.""" + payload = { + "required_route_carrier_names": [], + "required_route_carrier_source_names": [], + "required_route_carrier_source_labels": [], + "forbidden_route_carrier_names": [], + "forbidden_route_carrier_source_names": [], + "forbidden_route_carrier_source_labels": [], + } + + def append_once(key, value): + text = str(value or "").strip() + if text and text not in payload[key]: + payload[key].append(text) + + for carrier in collect_route_carriers(doc): + mode = _route_constraint_mode_value(getattr(carrier, "QetRouteConstraintMode", "")) + name = (getattr(carrier, "Name", "") or "").strip() + if not mode or not name: + continue + source_name = (getattr(carrier, "QetRouteSourceName", "") or "").strip() + source_label = (getattr(carrier, "QetRouteSourceLabel", "") or "").strip() + if mode == ROUTE_CONSTRAINT_MODE_REQUIRED: + if source_name: + # 一个草图/Draft 源对象可能生成多条 UserPath;必经源对象表示经过其中任一相关路径即可。 + append_once("required_route_carrier_source_names", source_name) + append_once("required_route_carrier_source_labels", source_label) + else: + append_once("required_route_carrier_names", name) + elif mode == ROUTE_CONSTRAINT_MODE_FORBIDDEN: + if source_name: + append_once("forbidden_route_carrier_source_names", source_name) + append_once("forbidden_route_carrier_source_labels", source_label) + else: + append_once("forbidden_route_carrier_names", name) + return payload + + +def collect_route_constraint_source_counts(doc): + """Count Required/Forbidden modes stored on route source objects for UI summaries.""" + counts = { + "required": 0, + "forbidden": 0, + } + if doc is None: + return counts + seen = set() + for obj in list(getattr(doc, "Objects", []) or []): + if obj is None or id(obj) in seen or is_route_carrier(obj): + continue + seen.add(id(obj)) + if not _source_kind_value(obj) and not _is_route_path_source_object(obj): + continue + mode = _route_constraint_mode_value(getattr(obj, "QetRouteConstraintMode", "")) + if mode == ROUTE_CONSTRAINT_MODE_REQUIRED: + counts["required"] += 1 + elif mode == ROUTE_CONSTRAINT_MODE_FORBIDDEN: + counts["forbidden"] += 1 + return counts + + +def _route_constraint_mode_value(mode): + text = str(mode or "").strip() + normalized = text.lower() + if normalized in { + ROUTE_CONSTRAINT_MODE_REQUIRED.lower(), + "must", + "mustpass", + "must_pass", + "requiredpass", + } or text in {"必须经过", "必经"}: + return ROUTE_CONSTRAINT_MODE_REQUIRED + if normalized in { + ROUTE_CONSTRAINT_MODE_FORBIDDEN.lower(), + "forbid", + "blocked", + "avoid", + } or text in {"禁止经过", "禁经", "禁止"}: + return ROUTE_CONSTRAINT_MODE_FORBIDDEN + return "" + + +def _set_route_constraint_mode(obj, mode): + TerminalObjects.ensure_string_property( + obj, + "QetRouteConstraintMode", + PROPERTY_GROUP, + "Route constraint mode for automatic routing", + mode, + ) + + +def _selected_route_carriers_for_constraint(doc, selection_ex): + carriers = [] + seen = set() + for item in selection_ex or []: + source = getattr(item, "Object", None) + if source is None: + continue + candidates = [source] if is_route_carrier(source) else _live_source_carriers(doc, source) + for carrier in candidates: + if carrier is None or not is_route_carrier(carrier) or id(carrier) in seen: + continue + seen.add(id(carrier)) + carriers.append(carrier) + return carriers + + +def mark_route_constraint_mode_from_selection(doc, selection_ex, mode): + normalized = _route_constraint_mode_value(mode) + marked = [] + seen_marked = set() + for item in selection_ex or []: + source = getattr(item, "Object", None) + if source is None: + continue + if not is_route_carrier(source): + _set_route_constraint_mode(source, normalized) + carriers = [source] if is_route_carrier(source) else _live_source_carriers(doc, source) + for carrier in carriers: + if carrier is None or not is_route_carrier(carrier) or id(carrier) in seen_marked: + continue + _set_route_constraint_mode(carrier, normalized) + source_name = (getattr(carrier, "QetRouteSourceName", "") or "").strip() + source_obj = _document_object_by_name(doc, source_name) + if source_obj is not None: + _set_route_constraint_mode(source_obj, normalized) + seen_marked.add(id(carrier)) + marked.append(carrier) + return marked + + +def set_route_carrier_capacity_from_selection(doc, selection_ex, capacity): + normalized = _normalized_route_capacity(capacity) + marked = [] + seen_marked = set() + source_count = 0 + seen_sources = set() + for item in selection_ex or []: + source = getattr(item, "Object", None) + if source is None: + continue + if not is_route_carrier(source) and id(source) not in seen_sources: + _set_route_carrier_capacity_value(source, normalized) + seen_sources.add(id(source)) + source_count += 1 + carriers = [source] if is_route_carrier(source) else _live_source_carriers(doc, source) + for carrier in carriers: + if carrier is None or not is_route_carrier(carrier) or id(carrier) in seen_marked: + continue + _set_route_carrier_capacity_value(carrier, normalized) + source_name = (getattr(carrier, "QetRouteSourceName", "") or "").strip() + source_obj = _document_object_by_name(doc, source_name) + if source_obj is not None and id(source_obj) not in seen_sources: + _set_route_carrier_capacity_value(source_obj, normalized) + seen_sources.add(id(source_obj)) + source_count += 1 + seen_marked.add(id(carrier)) + marked.append(carrier) + return { + "route_capacity": normalized, + "route_capacity_carriers": len(marked), + "route_capacity_sources": source_count, + } + + +def clear_all_route_constraint_modes(doc): + """Clear global Required/Forbidden route constraints stored in the FreeCAD document.""" + report = { + "route_constraint_carriers": 0, + "route_constraint_sources": 0, + } + if doc is None: + return report + seen = set() + for obj in list(getattr(doc, "Objects", []) or []): + if obj is None or id(obj) in seen: + continue + seen.add(id(obj)) + mode = str(getattr(obj, "QetRouteConstraintMode", "") or "").strip() + if not mode: + continue + _set_route_constraint_mode(obj, "") + # 源路径对象也可能保存约束;清空它才能避免重生成 carrier 后又继承旧规则。 + if is_route_carrier(obj): + report["route_constraint_carriers"] += 1 + else: + report["route_constraint_sources"] += 1 + return report + + def _network_summary_from_graph(network): kinds = {} for carrier in network.get("carriers", []) or []: @@ -3392,6 +4585,31 @@ def _routing_range_only_network_payload(summary): } +def _component_has_actionable_route_carriers(component): + kinds = component.get("carrier_kinds", {}) if isinstance(component, dict) else {} + if not isinstance(kinds, dict): + return False + actionable_kinds = { + 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, + ROUTE_CARRIER_KIND_AUXILIARY_PATH, + ROUTE_CARRIER_KIND_TERMINAL_ACCESS, + } + return any(int(kinds.get(kind, 0) or 0) > 0 for kind in actionable_kinds) + + +def _actionable_isolated_components(components): + actionable = [ + component + for component in components or [] + if isinstance(component, dict) and _component_has_actionable_route_carriers(component) + ] + return actionable if len(actionable) > 1 else [] + + def _route_graph_components(network): nodes = network.get("nodes", {}) or {} edges = network.get("edges", {}) or {} @@ -3466,6 +4684,97 @@ def _wire_duct_endpoint_breaks(network): return breaks +def _route_component_bridge_suggestion(component, components, network): + carrier_by_name = { + getattr(carrier, "Name", ""): carrier + for carrier in network.get("carriers", []) or [] + if getattr(carrier, "Name", "") + } + source_names = set(component.get("carrier_names", []) or []) + source_carriers = [] + for name in source_names: + carrier = carrier_by_name.get(name) + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() if carrier is not None else "" + if kind in {ROUTE_CARRIER_KIND_WIRE_DUCT, ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END}: + source_carriers.append(carrier) + if not source_carriers: + source_carriers = [carrier_by_name.get(name) for name in source_names if carrier_by_name.get(name) is not None] + + target_carriers = [] + for target_component in components or []: + if target_component is component or not bool(target_component.get("has_terminal_access", False)): + continue + for name in target_component.get("carrier_names", []) or []: + carrier = carrier_by_name.get(name) + if carrier is not None: + target_carriers.append((target_component, carrier)) + + best = None + for source in source_carriers: + source_points = _carrier_points(source) + if len(source_points) < 2: + continue + for target_component, target in target_carriers: + target_points = _carrier_points(target) + if len(target_points) < 2: + continue + nearest = _nearest_points_between_route_point_runs(source_points, target_points) + if nearest is None: + continue + distance, source_point, target_point = nearest + if best is None or distance < best[0]: + best = (distance, source, target, source_point, target_point, target_component) + + if best is None: + return {} + distance, source, target, source_point, target_point, target_component = best + return { + "distance_mm": float(distance), + "from_component_index": component.get("index"), + "to_component_index": target_component.get("index"), + "from_carrier": _carrier_track_payload(source), + "to_carrier": _carrier_track_payload(target), + "from_point": _point_payload(source_point), + "to_point": _point_payload(target_point), + "suggested_action": "create_user_path_bridge", + } + + +def _wire_duct_components_without_terminal_access(components, network=None): + has_terminal_access_network = any( + bool(component.get("has_terminal_access", False)) + for component in components or [] + if isinstance(component, dict) + ) + if not has_terminal_access_network: + return [] + result = [] + for component in components or []: + kinds = component.get("carrier_kinds", {}) if isinstance(component, dict) else {} + if not isinstance(kinds, dict): + continue + has_wire_duct = ( + int(kinds.get(ROUTE_CARRIER_KIND_WIRE_DUCT, 0) or 0) > 0 + or int(kinds.get(ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, 0) or 0) > 0 + ) + if not has_wire_duct or bool(component.get("has_terminal_access", False)): + continue + payload = { + "index": component.get("index"), + "nodes": int(component.get("nodes", 0) or 0), + "segments": int(component.get("segments", 0) or 0), + "carrier_kinds": dict(kinds), + "carrier_names": list(component.get("carrier_names", []) or [])[:12], + "code": "wire_duct_without_terminal_access", + } + if isinstance(network, dict): + suggestion = _route_component_bridge_suggestion(component, components, network) + if suggestion: + payload["bridge_suggestion"] = suggestion + result.append(payload) + return result + + def _invalid_route_carriers(network): invalid = [] for carrier in network.get("carriers", []) or []: @@ -3484,6 +4793,96 @@ def _invalid_route_carriers(network): return invalid +def _cabinet_interior_boundary_bboxes(doc): + bboxes = [] + for obj in list(getattr(doc, "Objects", []) or []): + if not is_routing_boundary(obj): + continue + bbox = _bound_box_from_object(obj) + if bbox is not None: + bboxes.append(bbox) + return bboxes + + +def _route_carriers_outside_boundary(network, boundary_bboxes): + if not boundary_bboxes: + return [] + outside = [] + for carrier in network.get("carriers", []) or []: + kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND + if kind == ROUTE_CARRIER_KIND_TERMINAL_ACCESS: + continue + points = _carrier_points(carrier) + outside_points = [ + point + for point in points + if not _point_inside_any_bbox(point, boundary_bboxes) + ] + if not outside_points: + continue + # 这里只检查路径源是否已经跑出柜内空间,真实导线结果仍由 AutoRouting 再做候选评分。 + outside.append( + { + "carrier": _carrier_track_payload(carrier), + "point_count": len(points), + "outside_point_count": len(outside_points), + "outside_points": [_point_payload(point) for point in outside_points[:5]], + "code": "route_carrier_outside_boundary", + } + ) + return outside + + +def _terminals_outside_boundary(terminals, boundary_bboxes, terminal_exit_length=20.0): + if not boundary_bboxes: + return [] + outside = [] + for terminal in terminals or []: + check_points = [] + try: + check_points.append(_vector(TerminalObjects.terminal_origin(terminal))) + except Exception: + pass + try: + access_points = terminal_access_path_points(terminal, terminal_exit_length) + except Exception: + access_points = [] + if access_points: + check_points.append(_vector(access_points[-1])) + outside_points = [ + point + for point in check_points + if not _point_inside_any_bbox(point, boundary_bboxes) + ] + if not outside_points: + continue + # 端子在柜外通常表示设备还没装进真实柜内位置,后续求路很容易产生长接入或柜外线。 + payload = _terminal_diagnostic_payload(terminal) + payload.update( + { + "outside_point_count": len(outside_points), + "outside_points": [_point_payload(point) for point in outside_points[:3]], + "code": "terminal_outside_boundary", + } + ) + outside.append(payload) + return outside + + +def _diagnostic_issue_codes(issues): + codes = [] + seen = set() + for issue in issues or []: + if not isinstance(issue, dict): + continue + code = str(issue.get("code", "") or "").strip() + if not code or code in seen: + continue + seen.add(code) + codes.append(code) + return codes + + def _polyline_length(points): total = 0.0 previous = None @@ -3496,18 +4895,61 @@ def _polyline_length(points): def _terminal_diagnostic_payload(terminal): - return { + payload = { "name": getattr(terminal, "Name", ""), "label": getattr(terminal, "Label", ""), "terminal_uuid": (getattr(terminal, "QetTerminalUuid", "") or "").strip(), "instance_id": (getattr(terminal, "QetInstanceId", "") or "").strip(), } + try: + origin = TerminalObjects.terminal_origin(terminal) + payload["terminal_origin"] = _point_payload(origin) + except Exception: + pass + # 长接入通常和设备装配位置或端子局部出线路径有关,带上父设备便于手测时直接定位。 + for parent in _terminal_parent_chain(terminal): + payload["parent_device_name"] = getattr(parent, "Name", "") or "" + payload["parent_device_label"] = getattr(parent, "Label", "") or "" + payload["parent_device_instance_id"] = ( + getattr(parent, "QetInstanceId", "") or "" + ).strip() + payload["parent_device_element_uuid"] = ( + getattr(parent, "QetElementUuid", "") or "" + ).strip() + break + return payload + + +def _terminal_access_geometry_payload(access_points): + points = [_vector(point) for point in list(access_points or [])] + payload = { + "terminal_access_points": [_point_payload(point) for point in points], + "terminal_access_dominant_axis": "", + "terminal_access_axis_lengths_mm": {"x": 0.0, "y": 0.0, "z": 0.0}, + } + if len(points) < 2: + return payload + axis_lengths = {"x": 0.0, "y": 0.0, "z": 0.0} + previous = points[0] + for current in points[1:]: + axis_lengths["x"] += abs(float(current.x) - float(previous.x)) + axis_lengths["y"] += abs(float(current.y) - float(previous.y)) + axis_lengths["z"] += abs(float(current.z) - float(previous.z)) + previous = current + dominant_axis = max(axis_lengths, key=lambda axis: axis_lengths[axis]) + payload["terminal_access_axis_lengths_mm"] = { + axis: float(length) + for axis, length in axis_lengths.items() + } + payload["terminal_access_dominant_axis"] = dominant_axis if axis_lengths[dominant_axis] > 0.0 else "" + return payload def diagnose_routing_path_network( doc, terminal_exit_length=20.0, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, + terminal_access_warning_distance=0.0, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): """Inspect the generated routing path network without routing wires.""" @@ -3517,15 +4959,21 @@ def diagnose_routing_path_network( network = build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance) components = _route_graph_components(network) summary = _network_summary_from_graph(network) - isolated_components = components if len(components) > 1 else [] + isolated_components = _actionable_isolated_components(components) unconnected_terminals = [] long_terminal_accesses = [] invalid_terminal_local_routes = [] routing_range_only_network = _routing_range_only_network_payload(summary) + boundary_bboxes = _cabinet_interior_boundary_bboxes(doc) + routable_terminals = _collect_routable_terminals(doc) max_distance = max(float(terminal_access_max_distance or 0.0), 0.0) - 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 _collect_routable_terminals(doc): + configured_warning_distance = max(float(terminal_access_warning_distance or 0.0), 0.0) + if configured_warning_distance > 0.0: + warning_distance = configured_warning_distance + 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: local_route_issue = _terminal_local_route_issue(terminal) if local_route_issue is not None: invalid_terminal_local_routes.append(local_route_issue) @@ -3565,10 +5013,18 @@ def diagnose_routing_path_network( "code": "terminal_access_long", } ) + payload.update(_terminal_access_geometry_payload(access_points)) long_terminal_accesses.append(payload) possible_breaks = _wire_duct_endpoint_breaks(network) + wire_ducts_without_terminal_access = _wire_duct_components_without_terminal_access(components, 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( + routable_terminals, + boundary_bboxes, + terminal_exit_length=terminal_exit_length, + ) issues = [] if int(summary.get("segments", 0) or 0) <= 0: issues.append( @@ -3606,6 +5062,15 @@ def diagnose_routing_path_network( "count": len(possible_breaks), } ) + if wire_ducts_without_terminal_access: + issues.append( + { + "severity": "warning", + "code": "wire_ducts_without_terminal_access", + "message": "Some wire duct components are not connected to terminal access carriers.", + "count": len(wire_ducts_without_terminal_access), + } + ) if long_terminal_accesses: issues.append( { @@ -3642,6 +5107,24 @@ def diagnose_routing_path_network( "count": len(invalid_route_carriers), } ) + if route_carriers_outside_boundary: + issues.append( + { + "severity": "warning", + "code": "route_carriers_outside_boundary", + "message": "Some route carriers have points outside cabinet interior boundaries.", + "count": len(route_carriers_outside_boundary), + } + ) + if terminals_outside_boundary: + issues.append( + { + "severity": "warning", + "code": "terminals_outside_boundary", + "message": "Some terminals are outside cabinet interior boundaries.", + "count": len(terminals_outside_boundary), + } + ) return { "summary": summary, @@ -3653,8 +5136,12 @@ def diagnose_routing_path_network( "invalid_terminal_local_routes": invalid_terminal_local_routes, "routing_range_only_network": routing_range_only_network, "invalid_route_carriers": invalid_route_carriers, + "route_carriers_outside_boundary": route_carriers_outside_boundary, + "terminals_outside_boundary": terminals_outside_boundary, "possible_breaks": possible_breaks, + "wire_ducts_without_terminal_access": wire_ducts_without_terminal_access, "issues": issues, + "issue_codes": _diagnostic_issue_codes(issues), "ok": not issues, } @@ -3681,6 +5168,12 @@ def _highlight_routing_network_diagnostics(doc, diagnostic): if item.get("name", "") ) unconnected_terminal_names.update(invalid_local_route_terminal_names) + outside_boundary_terminal_names = set( + item.get("name", "") + for item in diagnostic.get("terminals_outside_boundary", []) or [] + if item.get("name", "") + ) + unconnected_terminal_names.update(outside_boundary_terminal_names) break_carriers = set( item.get("carrier", {}).get("name", "") for item in diagnostic.get("possible_breaks", []) or [] @@ -3691,6 +5184,11 @@ def _highlight_routing_network_diagnostics(doc, diagnostic): for item in diagnostic.get("invalid_route_carriers", []) or [] if item.get("carrier", {}).get("name", "") ) + break_carriers.update( + item.get("carrier", {}).get("name", "") + for item in diagnostic.get("route_carriers_outside_boundary", []) or [] + if item.get("carrier", {}).get("name", "") + ) for obj in list(getattr(doc, "Objects", []) or []): name = getattr(obj, "Name", "") @@ -3723,17 +5221,179 @@ def _clear_routing_path_network_diagnostics(doc, group): return removed +def _diagnostic_items(value): + if not isinstance(value, list): + return [] + return [item for item in value if isinstance(item, dict)] + + +def _diagnostic_distance_text(value): + try: + return "{0:.1f} mm".format(float(value)) + except Exception: + return "未知距离" + + +def _diagnostic_int(value, fallback=0): + try: + return int(value or 0) + except Exception: + return int(fallback or 0) + + +def _diagnostic_terminal_text(sample): + if not isinstance(sample, dict): + return "未知端子" + return ( + str(sample.get("label", "") or "").strip() + or str(sample.get("terminal_display", "") or "").strip() + or str(sample.get("terminal_uuid", "") or "").strip() + or str(sample.get("name", "") or "").strip() + or "未知端子" + ) + + +def _routing_path_network_diagnostic_message(diagnostic): + if not isinstance(diagnostic, dict): + return "布线路径网络检查失败:诊断结果无效。" + summary = diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {} + issues = _diagnostic_items(diagnostic.get("issues", []) or []) + if not issues: + message = "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。".format( + summary.get("carriers", 0), + summary.get("segments", 0), + summary.get("nodes", 0), + ) + bridged_segments = _diagnostic_int(summary.get("bridged_segments", 0)) + if bridged_segments > 0: + message += " 自动桥接 {0} 段相邻/投影主路径。".format(bridged_segments) + return message + + message = "布线路径网络检查发现 {0} 类问题。".format(len(issues)) + if any(issue.get("code") == "empty_routing_path_network" for issue in issues): + message += "\n布线路径网络为空:没有可用路径段。" + unconnected = _diagnostic_items(diagnostic.get("unconnected_terminals", []) or []) + if unconnected: + sample = unconnected[0] + message += "\n端子未接入:{0},距离最近网络 {1},当前端子接入最大距离 {2}。".format( + _diagnostic_terminal_text(sample), + _diagnostic_distance_text(sample.get("nearest_network_distance_mm")), + _diagnostic_distance_text(sample.get("terminal_access_max_distance_mm")), + ) + long_accesses = _diagnostic_items(diagnostic.get("long_terminal_accesses", []) or []) + if long_accesses: + sample = long_accesses[0] + message += "\n端子接入过长:{0},接入段 {1}。".format( + _diagnostic_terminal_text(sample), + _diagnostic_distance_text(sample.get("terminal_access_length_mm")), + ) + invalid_carriers = _diagnostic_items(diagnostic.get("invalid_route_carriers", []) or []) + if invalid_carriers: + sample = invalid_carriers[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + message += "\n路径对象几何无效:{0}。".format( + carrier.get("label") or carrier.get("name") or "未知路径对象" + ) + outside_carriers = _diagnostic_items(diagnostic.get("route_carriers_outside_boundary", []) or []) + if outside_carriers: + sample = outside_carriers[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + message += "\n路径越出柜内边界:{0},越界点 {1} 个。".format( + carrier.get("label") or carrier.get("name") or "未知路径对象", + _diagnostic_int(sample.get("outside_point_count", 0)), + ) + outside_terminals = _diagnostic_items(diagnostic.get("terminals_outside_boundary", []) or []) + if outside_terminals: + sample = outside_terminals[0] + message += "\n端子越出柜内边界:{0},越界点 {1} 个。".format( + _diagnostic_terminal_text(sample), + _diagnostic_int(sample.get("outside_point_count", 0)), + ) + possible_breaks = _diagnostic_items(diagnostic.get("possible_breaks", []) or []) + if possible_breaks: + sample = possible_breaks[0] + carrier = sample.get("carrier", {}) if isinstance(sample.get("carrier", {}), dict) else {} + message += "\n线槽端点疑似断开:{0}。".format( + carrier.get("label") or carrier.get("name") or "未知线槽" + ) + wire_duct_components = _diagnostic_items(diagnostic.get("wire_ducts_without_terminal_access", []) or []) + if wire_duct_components: + sample = wire_duct_components[0] + carriers = sample.get("carrier_names") or [] + carrier_text = "、".join([str(item) for item in carriers[:3]]) if carriers else "未知线槽" + suggestion = sample.get("bridge_suggestion", {}) + if isinstance(suggestion, dict) and suggestion: + target = suggestion.get("to_carrier", {}) if isinstance(suggestion.get("to_carrier", {}), dict) else {} + target_text = target.get("label") or target.get("name") or "主网络" + message += "\n线槽未接入端子主网络:{0},建议桥接到 {1},距离 {2}。".format( + carrier_text, + target_text, + _diagnostic_distance_text(suggestion.get("distance_mm")), + ) + else: + message += "\n线槽未接入端子主网络:{0}。".format(carrier_text) + isolated = _diagnostic_items(diagnostic.get("isolated_components", []) or []) + if isolated: + sample = isolated[0] + carriers = sample.get("carrier_labels") or sample.get("carrier_names") or [] + carrier_text = "、".join([str(item) for item in carriers[:3]]) if carriers else "未知 carrier" + message += "\n存在孤立路径网络:{0}。".format(carrier_text) + return message + + +def _diagnostic_issue_codes_text(issue_codes): + values = [] + seen = set() + for code in list(issue_codes or []): + text = str(code or "").strip() + if not text or text in seen: + continue + seen.add(text) + values.append(text) + return ", ".join(values) + + +_ROUTING_PATH_NETWORK_ISSUE_LABELS = { + "empty_routing_path_network": "布线路径网络为空", + "invalid_route_carriers": "路径对象几何无效", + "routing_range_only_network": "仅使用布线面兜底", + "invalid_terminal_local_routes": "端子局部路径无效", + "route_carriers_outside_boundary": "路径越出柜内边界", + "terminals_outside_boundary": "端子越出柜内边界", + "long_terminal_accesses": "端子接入过长", + "unconnected_terminals": "端子未接入", + "wire_duct_endpoint_breaks": "线槽端点疑似断开", + "wire_ducts_without_terminal_access": "线槽未接入端子主网络", + "isolated_network_components": "存在孤立路径网络", +} + + +def _diagnostic_issue_labels_text(issue_codes): + values = [] + seen = set() + for code in list(issue_codes or []): + text = str(code or "").strip() + label = _ROUTING_PATH_NETWORK_ISSUE_LABELS.get(text, text) + if not label or label in seen: + continue + seen.add(label) + values.append(label) + return "、".join(values) + + def write_routing_path_network_diagnostic( doc, project_uuid="", terminal_exit_length=20.0, terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE, + terminal_access_warning_distance=0.0, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE, ): diagnostic = diagnose_routing_path_network( doc, terminal_exit_length=terminal_exit_length, terminal_access_max_distance=terminal_access_max_distance, + terminal_access_warning_distance=terminal_access_warning_distance, adjoining_duct_tolerance=adjoining_duct_tolerance, ) group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) @@ -3748,6 +5408,41 @@ def write_routing_path_network_diagnostic( "QET diagnostic kind", "RoutingPathNetwork", ) + TerminalObjects.ensure_string_property( + obj, + "QetProjectUuid", + PROPERTY_GROUP, + "Project UUID", + project_uuid, + ) + TerminalObjects.ensure_bool_property( + obj, + "QetDiagnosticOk", + PROPERTY_GROUP, + "QET diagnostic pass state", + bool(diagnostic.get("ok", False)), + ) + TerminalObjects.ensure_string_property( + obj, + "QetDiagnosticIssueCodes", + PROPERTY_GROUP, + "QET routing diagnostic issue codes", + _diagnostic_issue_codes_text(diagnostic.get("issue_codes", [])), + ) + TerminalObjects.ensure_string_property( + obj, + "QetDiagnosticIssueLabels", + PROPERTY_GROUP, + "QET routing diagnostic issue labels", + _diagnostic_issue_labels_text(diagnostic.get("issue_codes", [])), + ) + TerminalObjects.ensure_string_property( + obj, + "QetDiagnosticMessage", + PROPERTY_GROUP, + "QET routing path network diagnostic message", + _routing_path_network_diagnostic_message(diagnostic), + ) TerminalObjects.ensure_string_property( obj, "QetDiagnosticJson", @@ -3768,9 +5463,6 @@ def write_routing_path_network_diagnostic( def carrier_payload(carrier): - return { - "name": getattr(carrier, "Name", ""), - "label": getattr(carrier, "Label", ""), - "kind": getattr(carrier, "QetRouteCarrierKind", ""), - "points": [_point_payload(point) for point in _carrier_points(carrier)], - } + payload = _carrier_track_payload(carrier) + payload["points"] = [_point_payload(point) for point in _carrier_points(carrier)] + return payload diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 8372a37..327d7fe 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1,6 +1,9 @@ import importlib import json +import os +import sqlite3 import sys +import tempfile import types import unittest from pathlib import Path @@ -137,10 +140,11 @@ class FakeBoundBox: class FakeShape: - def __init__(self, bbox, edges=None, faces=None): + def __init__(self, bbox, edges=None, faces=None, wires=None): self.BoundBox = bbox self.Edges = edges or [] self.Faces = faces or [] + self.Wires = wires or [] self.Solids = [] self.Shells = [] @@ -157,6 +161,28 @@ class FakeEdge: self.Vertexes = [FakeVertex(start), FakeVertex(end)] +class FakeCurveEdge: + ShapeType = "Edge" + + def __init__(self, points): + self._points = list(points) + self.Vertexes = [FakeVertex(self._points[0]), FakeVertex(self._points[-1])] + + def discretize(self, *args, **kwargs): + return list(self._points) + + +class FakeWire: + ShapeType = "Wire" + + def __init__(self, points, edges=None): + self._points = list(points) + self.Edges = edges or [] + + def discretize(self, *args, **kwargs): + return list(self._points) + + class FakeFace: ShapeType = "Face" @@ -273,6 +299,8 @@ class AutoRoutingTest(unittest.TestCase): wire = result["wire"] payload = json.loads(wire.QetRouteDiagnosticsJson) + self.assertEqual("N4111: terminal-start -> terminal-end (Routed)", wire.Label) + self.assertEqual(wire.Label, result["wire_object_label"]) self.assertGreater(float(wire.QetRouteLengthMm), 0.0) self.assertEqual("42", wire.QetWireStyleId) self.assertEqual("42", payload["wire_style_id"]) @@ -281,35 +309,80 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("WireDuct", payload["route_track"]["segments"][0]["carrier"]["kind"]) self.assertTrue(json.loads(wire.QetRouteTrackJson)["carrier_names"]) - def test_route_track_preserves_generated_carrier_source_metadata(self): + def test_eplan_connection_route_applies_wire_style_from_wire_properties_database(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)) - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "线槽A" - duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL, + diameter_mm REAL, + area_or_spec TEXT + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width, diameter_mm, area_or_spec) + VALUES + (42, 'project-1', '蓝色控制线', '#3366CC', 3.5, 1.25, '1.25mm2') + """ + ) + connection.commit() + finally: + connection.close() - auto_routing_panel.AutoRoutingController().generate_routing_paths() - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - route_track = json.loads(result["wire"].QetRouteTrackJson) - wire_duct_carriers = [ - segment["carrier"] - for segment in route_track["segments"] - if segment["carrier"]["kind"] == "WireDuct" - ] + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + wire_label="N4111", + options={ + "wire_style_id": "42", + "wire_style_database_path": str(db_path), + }, + ) - self.assertTrue(wire_duct_carriers) - self.assertEqual("WireDuctA", wire_duct_carriers[0].get("source_name")) - self.assertEqual("线槽A", wire_duct_carriers[0].get("source_label")) - self.assertEqual("WireDuct", wire_duct_carriers[0].get("source_kind")) + wire = result["wire"] + style = json.loads(wire.QetWireStyleJson) + diagnostics = json.loads(wire.QetRouteDiagnosticsJson) - def test_route_track_records_carrier_capacity(self): + self.assertEqual((0.2, 0.4, 0.8), wire.ViewObject.LineColor) + self.assertEqual(3.5, wire.ViewObject.LineWidth) + self.assertEqual("Resolved", wire.QetWireStyleStatus) + self.assertEqual("蓝色控制线", wire.QetWireStyleName) + self.assertEqual("1.25mm2", wire.QetWireSpecText) + self.assertEqual("#3366CC", wire.QetWireColorText) + self.assertEqual("42", style["id"]) + self.assertEqual("蓝色控制线", style["name"]) + self.assertEqual("#3366CC", style["line_color"]) + self.assertEqual(3.5, style["line_width"]) + self.assertEqual("1.25mm2", style["area_or_spec"]) + self.assertEqual("Resolved", diagnostics["wire_style_status"]) + self.assertEqual(style, diagnostics["wire_style"]) + + def test_eplan_connection_route_reports_missing_wire_style(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -322,15 +395,30 @@ class AutoRoutingTest(unittest.TestCase): [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", - capacity=3, ) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - route_track = json.loads(result["wire"].QetRouteTrackJson) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-missing-style", + options={ + "wire_style_id": "404", + "wire_style_lookup": lambda _style_id, _project_uuid: {}, + }, + ) - self.assertEqual(3, route_track["segments"][0]["carrier"]["capacity"]) + wire = result["wire"] + diagnostics = json.loads(wire.QetRouteDiagnosticsJson) - def test_network_eplan_connection_route_offsets_lane_by_route_index(self): + self.assertEqual("404", wire.QetWireStyleId) + self.assertEqual("Missing", wire.QetWireStyleStatus) + self.assertEqual("Missing", diagnostics["wire_style_status"]) + self.assertNotIn("wire_style", diagnostics) + self.assertEqual((0.0, 0.35, 1.0), wire.ViewObject.LineColor) + self.assertEqual(5.0, wire.ViewObject.LineWidth) + + def test_eplan_connection_route_uses_wire_properties_database_from_environment(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -344,44 +432,55 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", kind="WireDuct", ) + previous = os.environ.get("QET_WIRE_PROPERTIES_DB") + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width) + VALUES + (7, 'project-1', '红色动力线', 'rgb(255,0,0)', 6.0) + """ + ) + connection.commit() + finally: + connection.close() + os.environ["QET_WIRE_PROPERTIES_DB"] = str(db_path) + try: + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-env-style", + options={"wire_style_id": "7"}, + ) + finally: + if previous is None: + os.environ.pop("QET_WIRE_PROPERTIES_DB", None) + else: + os.environ["QET_WIRE_PROPERTIES_DB"] = previous - first = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - route_index=0, - wire_uuid="wire-1", - options={"lane_spacing": 12.0, "lane_axis": "y"}, - ) - second = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - route_index=1, - wire_uuid="wire-2", - options={"lane_spacing": 12.0, "lane_axis": "y"}, - ) - third = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - route_index=2, - wire_uuid="wire-3", - options={"lane_spacing": 12.0, "lane_axis": "y"}, - ) - payload = json.loads(second["wire"].QetRouteDiagnosticsJson) - third_payload = json.loads(third["wire"].QetRouteDiagnosticsJson) + wire = result["wire"] - self.assertTrue(any(abs(point.y - 0.0) <= 0.001 for point in first["points"][1:-1])) - self.assertTrue(any(abs(point.y - 12.0) <= 0.001 for point in second["points"][1:-1])) - self.assertTrue(any(abs(point.y + 12.0) <= 0.001 for point in third["points"][1:-1])) - self.assertEqual(1, payload["lane"]["index"]) - self.assertEqual("y", payload["lane"]["axis"]) - self.assertEqual(12.0, payload["lane"]["offset_mm"]) - self.assertEqual(2, third_payload["lane"]["index"]) - self.assertEqual(-12.0, third_payload["lane"]["offset_mm"]) + self.assertEqual((1.0, 0.0, 0.0), wire.ViewObject.LineColor) + self.assertEqual(6.0, wire.ViewObject.LineWidth) + self.assertEqual("红色动力线", json.loads(wire.QetWireStyleJson)["name"]) - def test_network_eplan_connection_route_removes_collinear_network_points(self): + def test_eplan_connection_route_applies_wire_style_line_type(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -391,65 +490,131 @@ 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(50, 0, 20), - app.Vector(100, 0, 20), - ], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_type TEXT, + line_width REAL + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_type, line_width) + VALUES + (8, 'project-1', '虚线样式', '#00AA00', 'DashLine', 2.5) + """ + ) + connection.commit() + finally: + connection.close() - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "wire_style_id": "8", + "wire_style_database_path": str(db_path), + }, + ) - point_tuples = [(point.x, point.y, point.z) for point in result["points"]] - self.assertNotIn((50.0, 0.0, 20.0), point_tuples) - self.assertEqual( - [ - (0.0, 0.0, 0.0), - (0.0, 0.0, 20.0), - (100.0, 0.0, 20.0), - (100.0, 0.0, 0.0), - ], - point_tuples, - ) + wire = result["wire"] + style = json.loads(wire.QetWireStyleJson) - def test_eplan_connection_route_replaces_existing_wire_uuid_when_endpoints_change(self): + self.assertEqual("Dashed", wire.ViewObject.DrawStyle) + self.assertEqual("DashLine", style["line_type"]) + + def test_eplan_connection_route_accepts_bare_hex_color_and_diameter_width(self): _install_fake_freecad() - terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + 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_old = _terminal(doc, terminal_objects, "TerminalStartOld", "terminal-start-old", app.Vector(0, 0, 0)) - end_old = _terminal(doc, terminal_objects, "TerminalEndOld", "terminal-end-old", app.Vector(100, 0, 0)) - start_new = _terminal(doc, terminal_objects, "TerminalStartNew", "terminal-start-new", app.Vector(0, 40, 0)) - end_new = _terminal(doc, terminal_objects, "TerminalEndNew", "terminal-end-new", app.Vector(100, 40, 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(100, 40, 20), - app.Vector(0, 40, 20), - ], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL, + diameter_mm REAL + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width, diameter_mm) + VALUES + (12, 'project-1', '无井号十六进制颜色', '3366CC', NULL, 2.25), + (13, 'project-1', '0x十六进制颜色', '0x3366CC', NULL, 1.5) + """ + ) + connection.commit() + finally: + connection.close() - auto_routing.route_eplan_connection_between_terminals(doc, start_old, end_old, wire_uuid="wire-1") - auto_routing.route_eplan_connection_between_terminals(doc, start_new, end_new, wire_uuid="wire-1") - routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "wire_style_id": "12", + "wire_style_database_path": str(db_path), + }, + ) - self.assertEqual(1, len(routed_wires)) - self.assertEqual("terminal-start-new", routed_wires[0].QetStartTerminalUuid) - self.assertEqual("terminal-end-new", routed_wires[0].QetEndTerminalUuid) + wire = result["wire"] - def test_eplan_connection_route_keeps_existing_wire_when_replacement_fails(self): + self.assertEqual((0.2, 0.4, 0.8), wire.ViewObject.LineColor) + self.assertEqual(2.25, wire.ViewObject.LineWidth) + + second_result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "wire_style_id": "13", + "wire_style_database_path": str(db_path), + }, + ) + second_wire = second_result["wire"] + + self.assertEqual((0.2, 0.4, 0.8), second_wire.ViewObject.LineColor) + self.assertEqual(1.5, second_wire.ViewObject.LineWidth) + + def test_eplan_connection_route_estimates_width_from_wire_spec_area(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)) @@ -459,29 +624,53 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", kind="WireDuct", ) - first = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="wire-1", - )["wire"] - routing_network.clear_route_carriers(doc) + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL, + diameter_mm REAL, + area_or_spec TEXT + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width, diameter_mm, area_or_spec) + VALUES + (14, 'project-1', '按截面积估算', '#AA0000', NULL, NULL, '2.5mm2') + """ + ) + connection.commit() + finally: + connection.close() - with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_eplan_connection_between_terminals( + result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, - wire_uuid="wire-1", + options={ + "wire_style_id": "14", + "wire_style_database_path": str(db_path), + }, ) - routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) - self.assertEqual([first], routed_wires) - self.assertIsNotNone(doc.getObject(first.Name)) + wire = result["wire"] - def test_eplan_connection_route_keeps_existing_wire_when_new_geometry_creation_fails(self): + self.assertAlmostEqual(1.784, wire.ViewObject.LineWidth, places=3) + self.assertEqual("2.5mm2", wire.QetWireSpecText) + + def test_eplan_connection_route_accepts_argb_hex_color(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") @@ -493,36 +682,51 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", kind="WireDuct", ) - first = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="wire-1", - )["wire"] - original_create_wire_geometry = auto_routing._create_wire_geometry + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width) + VALUES + (15, 'project-1', 'ARGB颜色', '#FF3366CC', 2.0) + """ + ) + connection.commit() + finally: + connection.close() - def failing_create_wire_geometry(_doc, _name, _points): - raise RuntimeError("create failed") + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "wire_style_id": "15", + "wire_style_database_path": str(db_path), + }, + ) - auto_routing._create_wire_geometry = failing_create_wire_geometry - try: - with self.assertRaises(RuntimeError): - auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="wire-1", - ) - finally: - auto_routing._create_wire_geometry = original_create_wire_geometry - routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + wire = result["wire"] - self.assertEqual([first], routed_wires) - self.assertIsNotNone(doc.getObject(first.Name)) + self.assertEqual((0.2, 0.4, 0.8), wire.ViewObject.LineColor) + self.assertEqual("#FF3366CC", wire.QetWireColorText) - def test_eplan_connection_route_cleans_up_half_created_wire_when_draft_fallback_fails(self): + def test_eplan_connection_route_accepts_decimal_integer_color(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") @@ -534,260 +738,443 @@ class AutoRoutingTest(unittest.TestCase): project_uuid="project-1", kind="WireDuct", ) - first = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="wire-1", - )["wire"] - part_module = sys.modules["Part"] - draft_module = sys.modules.get("Draft") - if draft_module is None: - draft_module = types.ModuleType("Draft") - sys.modules["Draft"] = draft_module - original_make_polygon = part_module.makePolygon - original_make_wire = getattr(draft_module, "make_wire", None) - original_add_object = doc.addObject + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width) + VALUES + (16, 'project-1', '十进制颜色', '16711680', 2.0) + """ + ) + connection.commit() + finally: + connection.close() - def failing_make_polygon(*args, **kwargs): - raise RuntimeError("part unavailable") + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={ + "wire_style_id": "16", + "wire_style_database_path": str(db_path), + }, + ) - def half_created_make_wire(points, closed=False, placement=None, face=None, support=None, bs2wire=False): - obj = doc.addObject("Part::FeaturePython", "Wire") - obj.Points = list(points) - raise RuntimeError("draft failed") + wire = result["wire"] - def failing_add_object(type_name, name): - if type_name == "App::FeaturePython": - raise RuntimeError("fallback failed") - return original_add_object(type_name, name) - - part_module.makePolygon = failing_make_polygon - draft_module.make_wire = half_created_make_wire - doc.addObject = failing_add_object - try: - with self.assertRaises(RuntimeError): - auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="wire-1", - ) - finally: - part_module.makePolygon = original_make_polygon - if original_make_wire is None: - delattr(draft_module, "make_wire") - else: - draft_module.make_wire = original_make_wire - doc.addObject = original_add_object - - routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + self.assertEqual((1.0, 0.0, 0.0), wire.ViewObject.LineColor) + self.assertEqual("16711680", wire.QetWireColorText) - self.assertEqual([first], routed_wires) - self.assertEqual(0, len([obj for obj in doc.Objects if obj.Name == "Wire"])) - - def test_eplan_connection_route_keeps_existing_wire_when_old_replacement_removal_fails(self): + def test_route_eplan_connections_reuses_wire_style_lookup_for_same_style_id(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)) + _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, 40, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 40, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - first = auto_routing.route_eplan_connection_between_terminals( + routing_network.create_route_carrier( doc, - start, - end, - wire_uuid="wire-1", - )["wire"] - original_remove = auto_routing._remove_routing_connection_objects + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + calls = [] - def failing_remove(target_doc, objects): - if first in list(objects or []): - return 0 - return original_remove(target_doc, objects) + def lookup(style_id, project_uuid): + calls.append((style_id, project_uuid)) + return {"id": style_id, "name": "缓存样式", "line_color": "#00AA00", "line_width": 4.0} - auto_routing._remove_routing_connection_objects = failing_remove - try: - with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="wire-1", - ) - finally: - auto_routing._remove_routing_connection_objects = original_remove + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + "wire_style_id": 9, + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + "wire_style_id": 9, + }, + ], + } - routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"wire_style_lookup": lookup}, + ) - self.assertEqual([first], routed_wires) + self.assertEqual(2, report["routed"]) + self.assertEqual([("9", "project-1")], calls) - def test_route_carrier_styles_make_generated_objects_distinguishable(self): + def test_route_eplan_connections_reports_wire_style_status_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() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - - wire_duct = 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, 40, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 40, 0)) + routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - routing_range = routing_network.create_route_carrier( + routing_network.create_route_carrier( doc, - [app.Vector(0, 10, 0), app.Vector(100, 10, 0)], + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], project_uuid="project-1", - kind="RoutingRange", + kind="WireDuct", ) - terminal_access = routing_network.create_route_carrier( + + def lookup(style_id, _project_uuid): + if style_id == "1": + return {"id": "1", "name": "绿色控制线", "line_color": "#00AA00"} + return {} + + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-resolved-style", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + "wire_style_id": 1, + }, + { + "wire_id": "wire-missing-style", + "wire_label": "N2", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + "wire_style_id": 404, + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( doc, - [app.Vector(0, 20, 0), app.Vector(100, 20, 0)], - project_uuid="project-1", - kind="TerminalAccess", + payload, + options={"wire_style_lookup": lookup}, ) + compact = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) - self.assertEqual((1.0, 0.55, 0.0), wire_duct.ViewObject.LineColor) - self.assertEqual(4.0, wire_duct.ViewObject.LineWidth) - self.assertEqual((0.0, 0.65, 0.35), routing_range.ViewObject.LineColor) - self.assertEqual("Solid", routing_range.ViewObject.DrawStyle) - self.assertEqual((0.65, 0.2, 1.0), terminal_access.ViewObject.LineColor) - self.assertEqual("Solid", terminal_access.ViewObject.DrawStyle) - - def test_set_route_carriers_visibility_toggles_only_route_helpers(self): + self.assertEqual(2, report["routed"]) + self.assertEqual({"Resolved": 1, "Missing": 1}, report["wire_style_status_counts"]) + self.assertEqual("Resolved", report["routes"][0]["wire_style_status"]) + self.assertEqual("绿色控制线", report["routes"][0]["wire_style"]["name"]) + self.assertEqual("Missing", report["routes"][1]["wire_style_status"]) + self.assertEqual({"Resolved": 1, "Missing": 1}, compact["wire_style_status_counts"]) + self.assertEqual("绿色控制线", compact["route_samples"][0]["wire_style"]["name"]) + self.assertEqual("Missing", compact["route_samples"][1]["wire_style_status"]) + self.assertEqual("404", compact["route_samples"][1]["wire_style_id"]) + self.assertEqual(1, compact["missing_wire_style_samples_count"]) + self.assertEqual("N2", compact["missing_wire_style_samples"][0]["wire_label"]) + self.assertEqual("404", compact["missing_wire_style_samples"][0]["wire_style_id"]) + self.assertIn("导线样式:缺失 1 条", message) + self.assertIn("示例导线 N2 样式 404", message) + + def test_route_eplan_connections_uses_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() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - carrier = routing_network.create_route_carrier( + _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, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - device = doc.addObject("Part::Feature", "DeviceA") - device.ViewObject.Visibility = True + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.sqlite" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + name TEXT, + line_color TEXT, + line_width REAL + ) + """ + ) + connection.execute( + """ + INSERT INTO wire_properties + (id, project_uuid, name, line_color, line_width) + VALUES + (11, 'project-1', 'payload样式', '#8844FF', 4.5) + """ + ) + connection.commit() + finally: + connection.close() + payload = { + "project_uuid": "project-1", + "wire_style_database_path": str(db_path), + "wires": [ + { + "wire_id": "wire-payload-style", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": 11, + } + ], + } - hidden = routing_network.set_route_carriers_visibility(doc, False) - self.assertFalse(carrier.ViewObject.Visibility) - shown = routing_network.set_route_carriers_visibility(doc, True) + report = auto_routing.route_eplan_connections_from_payload(doc, payload) - self.assertEqual(1, hidden) - self.assertEqual(1, shown) - self.assertTrue(carrier.ViewObject.Visibility) - self.assertTrue(device.ViewObject.Visibility) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = list(getattr(routed_group, "Group", []) or [])[0] + compact = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) - def test_collect_route_carriers_ignores_deleted_object_references(self): + self.assertEqual(1, report["routed"]) + self.assertEqual(str(db_path), report["wire_style_database_path"]) + self.assertEqual(str(db_path), compact["wire_style_database_path"]) + self.assertIn("导线样式库:{0}".format(str(db_path)), message) + self.assertEqual((0.533333, 0.266667, 1.0), wire.ViewObject.LineColor) + self.assertEqual(4.5, wire.ViewObject.LineWidth) + self.assertEqual("payload样式", json.loads(wire.QetWireStyleJson)["name"]) + + def test_route_eplan_connections_uses_fallback_style_database_when_payload_database_is_empty(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") - carrier = routing_network.create_route_carrier( + _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, 0), app.Vector(100, 0, 0)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) + with tempfile.TemporaryDirectory() as temp_dir: + wrong_dir = Path(temp_dir) / "wrong" / "datafiles" + right_dir = Path(temp_dir) / "right" / "datafiles" + exchange_dir = Path(temp_dir) / "right" / ".qet_freecad" + wrong_dir.mkdir(parents=True) + right_dir.mkdir(parents=True) + exchange_dir.mkdir(parents=True) + wrong_db = wrong_dir / "project-local.db" + right_db = right_dir / "project-local.db" + for db_path, rows in ( + (wrong_db, []), + (right_db, [(1, "project-1", "红色动力线", "#ff0000", 4.0)]), + ): + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT, + name TEXT, + line_color TEXT, + line_width REAL + ) + """ + ) + connection.executemany( + "INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width) VALUES (?, ?, ?, ?, ?)", + rows, + ) + connection.commit() + finally: + connection.close() + json_path = exchange_dir / "2d_to_3d.json" + json_path.write_text( + json.dumps({"project_uuid": "project-1", "wires": []}), + encoding="utf-8", + ) + app._qet_exchange_summary = {"json_path": str(json_path)} + payload = { + "project_uuid": "project-1", + "wire_style_database_path": str(wrong_db), + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "1", + } + ], + } - class DeletedObjectReference: - Name = "DeletedCarrier" + report = auto_routing.route_eplan_connections_from_payload(doc, payload) - def __getattr__(self, name): - if name == "QetRoutingRole": - raise RuntimeError("Cannot access attribute 'QetRoutingRole' of deleted object") - raise AttributeError(name) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = list(getattr(routed_group, "Group", []) or [])[0] + compact = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) - doc.Objects.append(DeletedObjectReference()) + self.assertEqual(1, report["routed"]) + self.assertEqual(str(right_db), report["wire_style_database_path"]) + self.assertEqual(str(wrong_db), report["wire_style_database_fallback_from"]) + self.assertEqual(str(wrong_db), compact["wire_style_database_fallback_from"]) + self.assertIn("从备用库恢复", message) + self.assertIn(str(wrong_db), message) + self.assertEqual((1.0, 0.0, 0.0), wire.ViewObject.LineColor) + self.assertEqual(4.0, wire.ViewObject.LineWidth) + self.assertEqual("红色动力线", json.loads(wire.QetWireStyleJson)["name"]) - carriers = routing_network.collect_route_carriers(doc) + def test_route_track_preserves_generated_carrier_source_metadata(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)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "线槽A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) - self.assertEqual([carrier], carriers) + auto_routing_panel.AutoRoutingController().generate_routing_paths() + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + route_track = json.loads(result["wire"].QetRouteTrackJson) + wire_duct_carriers = [ + segment["carrier"] + for segment in route_track["segments"] + if segment["carrier"]["kind"] == "WireDuct" + ] - def test_route_carrier_exposes_capacity_property_for_auto_routing(self): + self.assertTrue(wire_duct_carriers) + self.assertEqual("WireDuctA", wire_duct_carriers[0].get("source_name")) + self.assertEqual("线槽A", wire_duct_carriers[0].get("source_label")) + self.assertEqual("WireDuct", wire_duct_carriers[0].get("source_kind")) + + def test_routed_wire_exposes_route_source_labels_for_manual_inspection(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)) carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="主线槽A", project_uuid="project-1", kind="WireDuct", ) + carrier.QetRouteSourceLabel = "黄色主路径" + carrier.QetRouteSourcePathIndex = "2" - self.assertIn("QetRouteCarrierCapacity", carrier.PropertiesList) - self.assertEqual(1, carrier.QetRouteCarrierCapacity) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) - def test_route_graph_connects_crossing_carriers_at_intersection(self): + wire = result["wire"] + self.assertEqual("黄色主路径(路径2)", wire.QetRouteSourceLabels) + self.assertEqual("QETRouteCarrier", wire.QetRouteCarrierNames) + payload = json.loads(wire.QetRouteDiagnosticsJson) + self.assertEqual(["黄色主路径(路径2)"], payload["route_source_labels"]) + self.assertEqual(["QETRouteCarrier"], payload["route_carrier_names"]) + + def test_route_track_preserves_user_path_source_path_index(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(50, 50, 0)) - 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( + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + user_path = routing_network.create_route_carrier( doc, - [app.Vector(50, -50, 0), app.Vector(50, 50, 0)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="用户路径B", project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) + user_path.QetRouteSourceName = "MultiWireRouteSketch" + user_path.QetRouteSourceLabel = "多路径草图" + user_path.QetRouteSourceKind = "UserPath" + user_path.QetRouteSourcePathIndex = "2" - network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + route_track = json.loads(result["wire"].QetRouteTrackJson) + user_path_carriers = [ + segment["carrier"] + for segment in route_track["segments"] + if segment["carrier"]["kind"] == "UserPath" + ] - self.assertEqual(5, len(network["nodes"])) - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertIn((50.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) + self.assertTrue(user_path_carriers) + self.assertEqual("2", user_path_carriers[0].get("source_path_index")) - def test_route_graph_connects_overlapping_collinear_carriers(self): + def test_carrier_payload_preserves_source_metadata(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(120, 0, 0)) - routing_network.create_route_carrier( + carrier = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 0), app.Vector(80, 0, 0)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="用户路径B", project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(40, 0, 0), app.Vector(120, 0, 0)], - project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) + carrier.QetRouteSourceName = "MultiWireRouteSketch" + carrier.QetRouteSourceLabel = "多路径草图" + carrier.QetRouteSourceKind = "UserPath" + carrier.QetRouteSourcePathIndex = "2" - network = routing_network.build_route_graph(doc) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + payload = routing_network.carrier_payload(carrier) - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertIn((40.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) - self.assertIn((80.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) - self.assertGreaterEqual(network["segment_count"], 3) + self.assertEqual("MultiWireRouteSketch", payload["source_name"]) + self.assertEqual("多路径草图", payload["source_label"]) + self.assertEqual("UserPath", payload["source_kind"]) + self.assertEqual("2", payload["source_path_index"]) - def test_route_graph_bridges_adjoining_wire_duct_gap_with_eplan_tolerance(self): + def test_route_track_records_carrier_capacity(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -797,195 +1184,211 @@ 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(50, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", + capacity=3, ) - network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + route_track = json.loads(result["wire"].QetRouteTrackJson) - self.assertEqual(1, network["bridged_segment_count"]) - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertEqual("Routed", result["route_status"]) + self.assertEqual(3, route_track["segments"][0]["carrier"]["capacity"]) - def test_route_graph_bridges_adjoining_user_path_to_wire_duct_gap(self): + def test_network_eplan_connection_route_offsets_lane_by_route_index(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, 20), app.Vector(50, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( + + first = auto_routing.route_eplan_connection_between_terminals( doc, - [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="UserPath", + start, + end, + route_index=0, + wire_uuid="wire-1", + options={"lane_spacing": 12.0, "lane_axis": "y"}, ) + second = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + route_index=1, + wire_uuid="wire-2", + options={"lane_spacing": 12.0, "lane_axis": "y"}, + ) + third = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + route_index=2, + wire_uuid="wire-3", + options={"lane_spacing": 12.0, "lane_axis": "y"}, + ) + payload = json.loads(second["wire"].QetRouteDiagnosticsJson) + third_payload = json.loads(third["wire"].QetRouteDiagnosticsJson) - network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) - start_key, _start_distance = routing_network.nearest_node(network, app.Vector(0, 0, 20)) - end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) - result = routing_network.shortest_path_with_carriers(network, start_key, end_key) - - self.assertEqual(1, network["bridged_segment_count"]) - self.assertIsNotNone(result) - self.assertIn("UserPath", result["carrier_kinds"]) + self.assertTrue(any(abs(point.y - 0.0) <= 0.001 for point in first["points"][1:-1])) + self.assertTrue(any(abs(point.y - 12.0) <= 0.001 for point in second["points"][1:-1])) + self.assertTrue(any(abs(point.y + 12.0) <= 0.001 for point in third["points"][1:-1])) + self.assertEqual(1, payload["lane"]["index"]) + self.assertEqual("y", payload["lane"]["axis"]) + self.assertEqual(12.0, payload["lane"]["offset_mm"]) + self.assertEqual(2, third_payload["lane"]["index"]) + self.assertEqual(-12.0, third_payload["lane"]["offset_mm"]) - def test_route_graph_bridges_endpoint_to_nearby_segment_projection(self): + def test_network_eplan_connection_route_removes_collinear_network_points(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, 20), app.Vector(100, 0, 20)], + [ + app.Vector(0, 0, 20), + app.Vector(50, 0, 20), + app.Vector(100, 0, 20), + ], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( - doc, - [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], - project_uuid="project-1", - kind="UserPath", - ) - network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) - start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) - end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) - result = routing_network.shortest_path_with_carriers(network, start_key, end_key) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual(1, network["bridged_segment_count"]) - self.assertIsNotNone(result) - self.assertIn((50000, 0, 20000), result["path"]) + point_tuples = [(point.x, point.y, point.z) for point in result["points"]] + self.assertNotIn((50.0, 0.0, 20.0), point_tuples) + self.assertEqual( + [ + (0.0, 0.0, 0.0), + (0.0, 0.0, 20.0), + (100.0, 0.0, 20.0), + (100.0, 0.0, 0.0), + ], + point_tuples, + ) - def test_auto_routing_uses_endpoint_to_segment_projection_bridge(self): + def test_eplan_connection_route_replaces_existing_wire_uuid_when_endpoints_change(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, "TerminalBranch", "terminal-branch", app.Vector(50, 50, 0)) - end = _terminal(doc, terminal_objects, "TerminalMain", "terminal-main", app.Vector(100, 0, 0)) + start_old = _terminal(doc, terminal_objects, "TerminalStartOld", "terminal-start-old", app.Vector(0, 0, 0)) + end_old = _terminal(doc, terminal_objects, "TerminalEndOld", "terminal-end-old", app.Vector(100, 0, 0)) + start_new = _terminal(doc, terminal_objects, "TerminalStartNew", "terminal-start-new", app.Vector(0, 40, 0)) + end_new = _terminal(doc, terminal_objects, "TerminalEndNew", "terminal-end-new", app.Vector(100, 40, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + [ + app.Vector(0, 0, 20), + app.Vector(100, 0, 20), + app.Vector(100, 40, 20), + app.Vector(0, 40, 20), + ], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( - doc, - [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], - project_uuid="project-1", - kind="UserPath", - ) - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - options={"adjoining_duct_tolerance": 15.0}, - ) + auto_routing.route_eplan_connection_between_terminals(doc, start_old, end_old, wire_uuid="wire-1") + auto_routing.route_eplan_connection_between_terminals(doc, start_new, end_new, wire_uuid="wire-1") + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(1, result["network"]["bridged_segments"]) - self.assertEqual(1, result["route_track"]["bridged_segments"]) - self.assertTrue(any(segment.get("is_bridge") for segment in result["route_track"]["segments"])) - self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + self.assertEqual(1, len(routed_wires)) + self.assertEqual("terminal-start-new", routed_wires[0].QetStartTerminalUuid) + self.assertEqual("terminal-end-new", routed_wires[0].QetEndTerminalUuid) - def test_auto_routing_does_not_use_terminal_access_to_bridge_main_path_gap(self): + def test_eplan_connection_route_keeps_existing_wire_when_replacement_fails(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(0, 0, 20), app.Vector(40, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="主线槽A", project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( - doc, - [app.Vector(40, 0, 20), app.Vector(60, 0, 20)], - project_uuid="project-1", - kind="TerminalAccess", - ) - routing_network.create_route_carrier( + first = auto_routing.route_eplan_connection_between_terminals( doc, - [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) + start, + end, + wire_uuid="wire-1", + )["wire"] + routing_network.clear_route_carriers(doc) with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={"terminal_access_max_distance": 5.0}, + wire_uuid="wire-1", ) + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) - def test_route_graph_projection_bridge_respects_blocked_bbox(self): + self.assertEqual([first], routed_wires) + self.assertIsNotNone(doc.getObject(first.Name)) + + def test_eplan_connection_route_keeps_existing_wire_when_new_geometry_creation_fails(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( + first = auto_routing.route_eplan_connection_between_terminals( doc, - [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], - project_uuid="project-1", - kind="UserPath", - ) - blocked_bboxes = [ - { - "xmin": 45.0, - "xmax": 55.0, - "ymin": 2.0, - "ymax": 6.0, - "zmin": 15.0, - "zmax": 25.0, - } - ] + start, + end, + wire_uuid="wire-1", + )["wire"] + original_create_wire_geometry = auto_routing._create_wire_geometry - network = routing_network.build_route_graph( - doc, - blocked_bboxes=blocked_bboxes, - adjoining_duct_tolerance=15.0, - ) - start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) - end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) - result = routing_network.shortest_path_with_carriers(network, start_key, end_key) + def failing_create_wire_geometry(_doc, _name, _points): + raise RuntimeError("create failed") - self.assertEqual(0, network["bridged_segment_count"]) - self.assertGreaterEqual(network["blocked_segment_count"], 1) - self.assertIsNone(result) + auto_routing._create_wire_geometry = failing_create_wire_geometry + try: + with self.assertRaises(RuntimeError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + finally: + auto_routing._create_wire_geometry = original_create_wire_geometry + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) - def test_auto_routing_respects_adjoining_duct_tolerance_option(self): + self.assertEqual([first], routed_wires) + self.assertIsNotNone(doc.getObject(first.Name)) + + def test_eplan_connection_route_cleans_up_half_created_wire_when_draft_fallback_fails(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") @@ -993,30 +1396,65 @@ 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(44, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( - doc, - [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - - result = auto_routing.route_eplan_connection_between_terminals( + first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, - options={"adjoining_duct_tolerance": 15.0}, - ) + wire_uuid="wire-1", + )["wire"] + part_module = sys.modules["Part"] + draft_module = sys.modules.get("Draft") + if draft_module is None: + draft_module = types.ModuleType("Draft") + sys.modules["Draft"] = draft_module + original_make_polygon = part_module.makePolygon + original_make_wire = getattr(draft_module, "make_wire", None) + original_add_object = doc.addObject - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(1, result["network"]["bridged_segments"]) + def failing_make_polygon(*args, **kwargs): + raise RuntimeError("part unavailable") - def test_auto_routing_uses_bridged_user_path_to_wire_duct_gap(self): + def half_created_make_wire(points, closed=False, placement=None, face=None, support=None, bs2wire=False): + obj = doc.addObject("Part::FeaturePython", "Wire") + obj.Points = list(points) + raise RuntimeError("draft failed") + + def failing_add_object(type_name, name): + if type_name == "App::FeaturePython": + raise RuntimeError("fallback failed") + return original_add_object(type_name, name) + + part_module.makePolygon = failing_make_polygon + draft_module.make_wire = half_created_make_wire + doc.addObject = failing_add_object + try: + with self.assertRaises(RuntimeError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + finally: + part_module.makePolygon = original_make_polygon + if original_make_wire is None: + delattr(draft_module, "make_wire") + else: + draft_module.make_wire = original_make_wire + doc.addObject = original_add_object + + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual([first], routed_wires) + self.assertEqual(0, len([obj for obj in doc.Objects if obj.Name == "Wire"])) + + def test_eplan_connection_route_keeps_existing_wire_when_old_replacement_removal_fails(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") @@ -1024,100 +1462,169 @@ 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(50, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( + first = auto_routing.route_eplan_connection_between_terminals( doc, - [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], + start, + end, + wire_uuid="wire-1", + )["wire"] + original_remove = auto_routing._remove_routing_connection_objects + + def failing_remove(target_doc, objects): + if first in list(objects or []): + return 0 + return original_remove(target_doc, objects) + + auto_routing._remove_routing_connection_objects = failing_remove + try: + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="wire-1", + ) + finally: + auto_routing._remove_routing_connection_objects = original_remove + + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual([first], routed_wires) + + def test_route_carrier_styles_make_generated_objects_distinguishable(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") + + wire_duct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", - kind="UserPath", + kind="WireDuct", ) - - result = auto_routing.route_eplan_connection_between_terminals( + routing_range = routing_network.create_route_carrier( doc, - start, - end, - options={"adjoining_duct_tolerance": 15.0}, + [app.Vector(0, 10, 0), app.Vector(100, 10, 0)], + project_uuid="project-1", + kind="RoutingRange", + ) + terminal_access = routing_network.create_route_carrier( + doc, + [app.Vector(0, 20, 0), app.Vector(100, 20, 0)], + project_uuid="project-1", + kind="TerminalAccess", ) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(1, result["network"]["bridged_segments"]) - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + self.assertEqual((1.0, 0.55, 0.0), wire_duct.ViewObject.LineColor) + self.assertEqual(4.0, wire_duct.ViewObject.LineWidth) + self.assertEqual((0.0, 0.65, 0.35), routing_range.ViewObject.LineColor) + self.assertEqual("Solid", routing_range.ViewObject.DrawStyle) + self.assertEqual((0.65, 0.2, 1.0), terminal_access.ViewObject.LineColor) + self.assertEqual("Solid", terminal_access.ViewObject.DrawStyle) - def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self): + def test_set_route_carriers_visibility_toggles_only_route_helpers(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() - routing_network.create_route_carrier( + terminal_objects.ensure_root_group(doc, "project-1") + carrier = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(50, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_route_carrier( + device = doc.addObject("Part::Feature", "DeviceA") + device.ViewObject.Visibility = True + + hidden = routing_network.set_route_carriers_visibility(doc, False) + self.assertFalse(carrier.ViewObject.Visibility) + shown = routing_network.set_route_carriers_visibility(doc, True) + + self.assertEqual(1, hidden) + self.assertEqual(1, shown) + self.assertTrue(carrier.ViewObject.Visibility) + self.assertTrue(device.ViewObject.Visibility) + + def test_collect_route_carriers_ignores_deleted_object_references(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(54, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) - network = routing_network.build_route_graph(doc) - original_keys = set(network["nodes"].keys()) - bridge_keys = { - key - for key, point in network["nodes"].items() - if point.x in {50.0, 54.0} - } - projected_key, _distance, mode = routing_network.connect_point_to_network(network, app.Vector(52, 0, 20)) - new_keys = set(network["nodes"].keys()) - original_keys - stale_bridge_edges = [ - (left_key, right_key) - for left_key, neighbors in network["edges"].items() - for right_key, _weight, _carrier in neighbors - if left_key in bridge_keys and right_key in bridge_keys - ] + class DeletedObjectReference: + Name = "DeletedCarrier" - self.assertEqual("segment_projection", mode) - self.assertEqual(projected_key, next(iter(new_keys))) - self.assertEqual([], stale_bridge_edges) - self.assertEqual(4, network["segment_count"]) + def __getattr__(self, name): + if name == "QetRoutingRole": + raise RuntimeError("Cannot access attribute 'QetRoutingRole' of deleted object") + raise AttributeError(name) - def test_eplan_connection_route_prefers_wire_duct_over_auxiliary_range(self): + doc.Objects.append(DeletedObjectReference()) + + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual([carrier], carriers) + + def test_route_carrier_exposes_capacity_property_for_auto_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") + + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + self.assertIn("QetRouteCarrierCapacity", carrier.PropertiesList) + self.assertEqual(1, carrier.QetRouteCarrierCapacity) + + def test_route_graph_connects_crossing_carriers_at_intersection(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(50, 50, 0)) routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", - kind="RoutingRange", + kind="WireDuct", ) routing_network.create_route_carrier( doc, - [ - app.Vector(0, 0, 20), - app.Vector(0, 40, 20), - app.Vector(120, 40, 20), - app.Vector(120, 0, 20), - ], + [app.Vector(50, -50, 0), app.Vector(50, 50, 0)], project_uuid="project-1", kind="WireDuct", ) + network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + self.assertEqual(5, len(network["nodes"])) self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertTrue(any(point.y == 40.0 for point in result["points"])) + self.assertIn((50.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) - def test_surface_carrier_grid_supports_backplate_routing(self): + def test_route_graph_connects_overlapping_collinear_carriers(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -1125,1261 +1632,1400 @@ 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(120, 0, 0)) - face = FakeFace( - FakeBoundBox(0, 120, -20, 120, -1, -1), - app.Vector(0, 0, 1), + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(80, 0, 0)], + project_uuid="project-1", + kind="WireDuct", ) - - created = routing_network.create_surface_carriers_from_selection( + routing_network.create_route_carrier( doc, - [FakeSelectionItem([face])], + [app.Vector(40, 0, 0), app.Vector(120, 0, 0)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) + + network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertGreater(len(created), 0) - self.assertEqual("RoutingRange", getattr(created[0], "QetRouteCarrierKind", "")) self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertTrue(any(point.z == 4.0 for point in result["points"])) + self.assertIn((40.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) + self.assertIn((80.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) + self.assertGreaterEqual(network["segment_count"], 3) - def test_auto_detect_support_surface_creates_routing_range(self): + def test_route_graph_bridges_adjoining_wire_duct_gap_with_eplan_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() + app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "安装板A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - cabinet = doc.addObject("Part::Feature", "Cabinet") - cabinet.Label = "3D机柜" - cabinet.Shape = FakeShape(FakeBoundBox(0, 300, 0, 80, 0, 400)) - duct = doc.addObject("Part::Feature", "WireDuctA") - duct.Label = "Wire Duct A" - duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - - created = routing_network.create_surface_carriers_from_document( + 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(50, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - created_again = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - self.assertEqual(6, len(created)) - self.assertEqual(0, len(created_again)) - self.assertTrue(all(carrier.QetRouteCarrierKind == "RoutingRange" for carrier in created)) - self.assertEqual("RoutingRange", panel.QetRoutingSourceKind) - self.assertEqual("SupportSurface", panel.QetRoutingObstacleMode) - self.assertFalse(hasattr(cabinet, "QetRoutingSourceKind")) - self.assertFalse(hasattr(duct, "QetRoutingSourceKind")) + network = routing_network.build_route_graph(doc) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - def test_auto_detect_support_surface_refreshes_routing_range_geometry(self): + self.assertEqual(1, network["bridged_segment_count"]) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertEqual("Routed", result["route_status"]) + + def test_route_graph_bridges_adjoining_user_path_to_wire_duct_gap(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") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "安装板A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - - created = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(0, 0, 20), app.Vector(50, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - panel.Shape = FakeShape(FakeBoundBox(20, 140, 0, 5, 0, 100)) - created_again = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="UserPath", ) - carriers = routing_network.collect_route_carriers(doc) - x_values = [ - point.x - for carrier in carriers - if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" - for point in carrier.Points - ] - self.assertEqual(6, len(created)) - self.assertEqual(0, len(created_again)) - self.assertEqual(6, len([carrier for carrier in carriers if carrier.QetRouteCarrierKind == "RoutingRange"])) - self.assertEqual(20.0, min(x_values)) - self.assertEqual(140.0, max(x_values)) + network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) + start_key, _start_distance = routing_network.nearest_node(network, app.Vector(0, 0, 20)) + end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) + result = routing_network.shortest_path_with_carriers(network, start_key, end_key) - def test_auto_detect_support_surface_adds_missing_routing_range_lanes_after_resize(self): + self.assertEqual(1, network["bridged_segment_count"]) + self.assertIsNotNone(result) + self.assertIn("UserPath", result["carrier_kinds"]) + + def test_route_graph_bridges_endpoint_to_nearby_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") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "安装板A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - - created = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - panel.Shape = FakeShape(FakeBoundBox(0, 180, 0, 5, 0, 120)) - created_again = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="UserPath", ) - carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" - ] - x_values = [point.x for carrier in carriers for point in carrier.Points] - z_values = [point.z for carrier in carriers for point in carrier.Points] - self.assertEqual(6, len(created)) - self.assertEqual(1, len(created_again)) - self.assertEqual(7, len(carriers)) - self.assertEqual(180.0, max(x_values)) - self.assertEqual(120.0, max(z_values)) + network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) + start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) + end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) + result = routing_network.shortest_path_with_carriers(network, start_key, end_key) - def test_auto_detect_support_surface_removes_stale_routing_range_lanes_after_resize(self): + self.assertEqual(1, network["bridged_segment_count"]) + self.assertIsNotNone(result) + self.assertIn((50000, 0, 20000), result["path"]) + + def test_auto_routing_uses_endpoint_to_segment_projection_bridge(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") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "安装板A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - - created = routing_network.create_surface_carriers_from_document( + start = _terminal(doc, terminal_objects, "TerminalBranch", "terminal-branch", app.Vector(50, 50, 0)) + end = _terminal(doc, terminal_objects, "TerminalMain", "terminal-main", 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", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - panel.Shape = FakeShape(FakeBoundBox(0, 60, 0, 5, 0, 60)) - created_again = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="UserPath", ) - carriers = [ - carrier - for carrier in routing_network.collect_route_carriers(doc) - if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" - ] - x_values = [point.x for carrier in carriers for point in carrier.Points] - z_values = [point.z for carrier in carriers for point in carrier.Points] - self.assertEqual(6, len(created)) - self.assertEqual(0, len(created_again)) - self.assertEqual(4, len(carriers)) - self.assertEqual(60.0, max(x_values)) - self.assertEqual(60.0, max(z_values)) + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"adjoining_duct_tolerance": 15.0}, + ) - def test_auto_detect_support_surface_removes_carriers_and_obstacle_mode_when_source_invalid(self): + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertEqual(1, result["network"]["bridged_segments"]) + self.assertEqual(1, result["route_track"]["bridged_segments"]) + self.assertTrue(any(segment.get("is_bridge") for segment in result["route_track"]["segments"])) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + + def test_auto_routing_does_not_use_terminal_access_to_bridge_main_path_gap(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") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "安装板A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - - created = routing_network.create_surface_carriers_from_document( + 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(40, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 120, 0, 120)) - created_again = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(40, 0, 20), app.Vector(60, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="TerminalAccess", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", ) - self.assertEqual(6, len(created)) - self.assertEqual(0, len(created_again)) - self.assertEqual([], routing_network.collect_route_carriers(doc)) - self.assertEqual("", getattr(panel, "QetRoutingObstacleMode", "")) - self.assertEqual("", getattr(panel, "QetRouteCarrierNamesJson", "")) + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_access_max_distance": 5.0}, + ) - def test_eplan_connection_route_can_use_auto_detected_support_surface(self): + def test_route_graph_projection_bridge_respects_blocked_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() 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, 10, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 10, 0)) - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "Mounting Plate A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - - created = routing_network.create_surface_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", - spacing=60.0, - offset=5.0, - margin=0.0, + kind="WireDuct", ) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + routing_network.create_route_carrier( + doc, + [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], + project_uuid="project-1", + kind="UserPath", + ) + blocked_bboxes = [ + { + "xmin": 45.0, + "xmax": 55.0, + "ymin": 2.0, + "ymax": 6.0, + "zmin": 15.0, + "zmax": 25.0, + } + ] - self.assertGreater(len(created), 0) - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) - self.assertTrue(any(point.y == 10.0 for point in result["points"])) - - def test_prepare_layout_space_auto_detects_support_surface_sources(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") - panel = doc.addObject("Part::Feature", "MountingPlateA") - panel.Label = "Mounting Plate A" - panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - - result = auto_routing_panel.AutoRoutingController().generate_layout_space() + network = routing_network.build_route_graph( + doc, + blocked_bboxes=blocked_bboxes, + adjoining_duct_tolerance=15.0, + ) + start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) + end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) + result = routing_network.shortest_path_with_carriers(network, start_key, end_key) - self.assertGreater(result["support_surface_sources"], 0) - self.assertEqual("document", result["source_mode"]) + self.assertEqual(0, network["bridged_segment_count"]) + self.assertGreaterEqual(network["blocked_segment_count"], 1) + self.assertIsNone(result) - def test_generate_routing_paths_uses_selected_wire_duct_entity(self): + def test_auto_routing_respects_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"] - gui = sys.modules["FreeCADGui"] 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)) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + 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(44, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", ) - result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"adjoining_duct_tolerance": 15.0}, + ) - self.assertEqual(1, result["wire_duct_carriers"]) - self.assertEqual("selection", result["source_mode"]) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(1, result["network"]["bridged_segments"]) - def test_generate_routing_paths_uses_selected_route_path_as_user_path(self): + def test_auto_routing_uses_bridged_user_path_to_wire_duct_gap(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"] - 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.Label = "用户主路径A" - route_path.Shape = FakeShape( - FakeBoundBox(0, 100, 0, 80, 20, 20), - edges=[ - FakeEdge(app.Vector(0, 0, 20), app.Vector(0, 80, 20)), - FakeEdge(app.Vector(0, 80, 20), app.Vector(100, 80, 20)), - ], + 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(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", ) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + routing_network.create_route_carrier( + doc, + [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="UserPath", ) - result = auto_routing_panel.AutoRoutingController().generate_routing_paths() - carriers = routing_network.collect_route_carriers(doc) - user_paths = [item for item in carriers if item.QetRouteCarrierKind == "UserPath"] + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"adjoining_duct_tolerance": 15.0}, + ) - self.assertEqual(1, result["user_path_carriers"]) - self.assertEqual(1, len(user_paths)) - self.assertEqual("UserRouteSketch", user_paths[0].QetRouteSourceName) - self.assertEqual("用户主路径A", user_paths[0].QetRouteSourceLabel) - self.assertEqual("selection", result["source_mode"]) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(1, result["network"]["bridged_segments"]) + self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) - def test_controller_creates_selected_user_paths_without_full_network_generation(self): + def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(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"] - 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, 0, 20), app.Vector(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", ) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + routing_network.create_route_carrier( + doc, + [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", ) + network = routing_network.build_route_graph(doc) + original_keys = set(network["nodes"].keys()) + bridge_keys = { + key + for key, point in network["nodes"].items() + if point.x in {50.0, 54.0} + } - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carriers = routing_network.collect_route_carriers(doc) + projected_key, _distance, mode = routing_network.connect_point_to_network(network, app.Vector(52, 0, 20)) + new_keys = set(network["nodes"].keys()) - original_keys + stale_bridge_edges = [ + (left_key, right_key) + for left_key, neighbors in network["edges"].items() + for right_key, _weight, _carrier in neighbors + if left_key in bridge_keys and right_key in bridge_keys + ] - self.assertEqual(1, result["user_path_carriers"]) - self.assertEqual(1, result["network"]["kinds"]["UserPath"]) - self.assertEqual(1, len(carriers)) - self.assertEqual("UserPath", carriers[0].QetRouteCarrierKind) + self.assertEqual("segment_projection", mode) + self.assertEqual(projected_key, next(iter(new_keys))) + self.assertEqual([], stale_bridge_edges) + self.assertEqual(4, network["segment_count"]) - def test_selected_points_object_can_be_used_as_user_path(self): + def test_eplan_connection_route_prefers_wire_duct_over_auxiliary_range(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"] - 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), - ] - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + 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)], + project_uuid="project-1", + kind="RoutingRange", + ) + routing_network.create_route_carrier( + doc, + [ + app.Vector(0, 0, 20), + app.Vector(0, 40, 20), + app.Vector(120, 40, 20), + app.Vector(120, 0, 20), + ], + project_uuid="project-1", + kind="WireDuct", ) - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carriers = routing_network.collect_route_carriers(doc) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - 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("network-dijkstra-v1", result["algorithm"]) + self.assertTrue(any(point.y == 40.0 for point in result["points"])) - def test_selected_user_path_copies_source_capacity(self): + def test_surface_carrier_grid_supports_backplate_routing(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"] - 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)], + 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)) + face = FakeFace( + FakeBoundBox(0, 120, -20, 120, -1, -1), + app.Vector(0, 0, 1), ) - auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - carrier = routing_network.collect_route_carriers(doc)[0] + created = routing_network.create_surface_carriers_from_selection( + doc, + [FakeSelectionItem([face])], + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - self.assertEqual(5, carrier.QetRouteCarrierCapacity) + self.assertGreater(len(created), 0) + self.assertEqual("RoutingRange", getattr(created[0], "QetRouteCarrierKind", "")) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertTrue(any(point.z == 4.0 for point in result["points"])) - def test_controller_create_user_paths_reports_removed_stale_source_carriers(self): + def test_auto_detect_support_surface_creates_routing_range(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))], - ) - 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: [], - ) - - result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - - self.assertEqual(1, result["removed_stale_carriers"]) - self.assertEqual(0, result["network"]["carriers"]) - self.assertEqual([], routing_network.collect_route_carriers(doc)) + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + cabinet = doc.addObject("Part::Feature", "Cabinet") + cabinet.Label = "3D机柜" + cabinet.Shape = FakeShape(FakeBoundBox(0, 300, 0, 80, 0, 400)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - 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 = _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( + created = routing_network.create_surface_carriers_from_document( doc, - [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], project_uuid="project-1", - kind="UserPath", + spacing=60.0, + offset=5.0, + margin=0.0, ) - - created = routing_network.create_terminal_access_carriers_from_document( + created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=100.0, + spacing=60.0, + offset=5.0, + margin=0.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(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertTrue(all(carrier.QetRouteCarrierKind == "RoutingRange" for carrier in created)) + self.assertEqual("RoutingRange", panel.QetRoutingSourceKind) + self.assertEqual("SupportSurface", panel.QetRoutingObstacleMode) + self.assertFalse(hasattr(cabinet, "QetRoutingSourceKind")) + self.assertFalse(hasattr(duct, "QetRoutingSourceKind")) - def test_generate_routing_paths_refreshes_selected_user_path_without_duplicate(self): + def test_auto_detect_support_surface_includes_cabinet_side_cover(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))], - ) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], - ) + side_cover = doc.addObject("Part::Feature", "SideCover") + side_cover.Label = "SIDE COVER-1_P00" + side_cover.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - 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))], + created = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, ) - 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, first["user_path_carriers"]) - self.assertEqual(0, 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.assertGreater(len(created), 0) + self.assertEqual("RoutingRange", side_cover.QetRoutingSourceKind) + self.assertEqual("SupportSurface", side_cover.QetRoutingObstacleMode) - def test_eplan_connection_route_can_use_generated_user_path(self): + def test_support_surface_source_capacity_is_copied_to_generated_carriers(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(200, 0, 0)) - routing_network.create_route_carrier( + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + panel.QetRouteCarrierCapacity = 6 + + created = routing_network.create_surface_carriers_from_document( doc, - [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], project_uuid="project-1", - kind="UserPath", + spacing=60.0, + offset=5.0, + margin=0.0, ) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) - - self.assertEqual("Routed", result["route_status"]) - self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + self.assertGreater(len(created), 0) + self.assertTrue(all(carrier.QetRouteCarrierCapacity == 6 for carrier in created)) - def test_generate_routing_paths_does_not_duplicate_selected_wire_duct_carriers(self): + def test_cabinet_boundary_objects_are_not_route_sources(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", "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() - second = auto_routing_panel.AutoRoutingController().generate_routing_paths() - carriers = routing_network.collect_route_carriers(doc) + boundary_duct = doc.addObject("Part::Feature", "BoundaryWireDuct") + boundary_duct.Label = "Wire Duct Boundary" + boundary_duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + boundary_duct.QetRoutingBoundaryKind = "CabinetInterior" + boundary_panel = doc.addObject("Part::Feature", "BoundaryMountingPlate") + boundary_panel.Label = "安装板边界" + boundary_panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + boundary_panel.QetRoutingBoundaryKind = "CabinetInterior" + boundary_path = doc.addObject("Part::Feature", "BoundaryPointPath") + boundary_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] + boundary_path.QetRoutingBoundaryKind = "CabinetInterior" - 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"]), + wire_ducts = routing_network.create_wire_duct_carriers_from_document( + doc, + project_uuid="project-1", ) - self.assertEqual( - 2, - len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), + surfaces = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + user_paths = routing_network.create_user_path_carriers_from_selection( + doc, + [FakeSelectionItem(obj=boundary_path)], + project_uuid="project-1", ) - def test_generate_routing_paths_refreshes_selected_wire_duct_geometry(self): + self.assertEqual([], wire_ducts) + self.assertEqual([], surfaces) + self.assertEqual([], user_paths) + self.assertEqual([], routing_network.collect_route_carriers(doc)) + + def test_detect_user_path_sources_skips_origin_axes_with_huge_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"] - gui = sys.modules["FreeCADGui"] 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)) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + axis = doc.addObject("App::Line", "X_Axis") + axis.Label = "X轴" + axis.Shape = FakeShape( + FakeBoundBox(-1e100, 1e100, 0, 0, 0, 0), + edges=[FakeEdge(app.Vector(0, 0, 0), app.Vector(1, 0, 0))], ) + route_path = doc.addObject("Part::FeaturePython", "UserRoutePath") + route_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] - 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 - ) + sources = routing_network.detect_user_path_sources(doc) - 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(["UserRoutePath"], [source.Name for source in sources]) - def test_generate_routing_paths_removes_generated_wire_duct_carriers_after_source_deleted(self): + def test_detect_user_path_sources_skips_existing_route_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"] 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)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) - 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() + sources = routing_network.detect_user_path_sources(doc) - self.assertEqual(3, len(generated)) - self.assertEqual([], routing_network.collect_route_carriers(doc)) + self.assertEqual([], sources) - def test_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self): + def test_auto_detect_support_surface_refreshes_routing_range_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"] + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() 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.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - gui.Selection = types.SimpleNamespace( - getSelection=lambda: [], - getSelectionEx=lambda: [FakeSelectionItem(obj=panel)], - ) - result = auto_routing_panel.AutoRoutingController().generate_layout_space() + created = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + panel.Shape = FakeShape(FakeBoundBox(20, 140, 0, 5, 0, 100)) + created_again = routing_network.create_surface_carriers_from_document( + doc, + project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, + ) + carriers = routing_network.collect_route_carriers(doc) + x_values = [ + point.x + for carrier in carriers + if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" + for point in carrier.Points + ] - self.assertGreater(result["support_surface_sources"], 0) - self.assertEqual("document", result["source_mode"]) + self.assertEqual(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual(6, len([carrier for carrier in carriers if carrier.QetRouteCarrierKind == "RoutingRange"])) + self.assertEqual(20.0, min(x_values)) + self.assertEqual(140.0, max(x_values)) - def test_generate_routing_path_network_adds_terminal_access_to_route_network(self): + def test_auto_detect_support_surface_adds_missing_routing_range_lanes_after_resize(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)) + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - 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" - ] - - 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() - 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, "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(0, 1, 20), app.Vector(5, 1, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( + created = routing_network.create_surface_carriers_from_document( 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", - kind="WireDuct", + spacing=60.0, + offset=5.0, + margin=0.0, ) - - created = routing_network.create_terminal_access_carriers_from_document( + panel.Shape = FakeShape(FakeBoundBox(0, 180, 0, 5, 0, 120)) + created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.0, ) + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" + ] + x_values = [point.x for carrier in carriers for point in carrier.Points] + z_values = [point.z for carrier in carriers for point in carrier.Points] - 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(6, len(created)) + self.assertEqual(1, len(created_again)) + self.assertEqual(7, len(carriers)) + self.assertEqual(180.0, max(x_values)) + self.assertEqual(120.0, max(z_values)) - def test_connection_entry_candidates_prefer_wire_duct_over_terminal_access(self): + def test_auto_detect_support_surface_removes_stale_routing_range_lanes_after_resize(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( + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( doc, - [app.Vector(0, 0, 20), app.Vector(0, 10, 20)], project_uuid="project-1", - kind="TerminalAccess", + spacing=60.0, + offset=5.0, + margin=0.0, ) - routing_network.create_route_carrier( + panel.Shape = FakeShape(FakeBoundBox(0, 60, 0, 5, 0, 60)) + created_again = routing_network.create_surface_carriers_from_document( 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), + spacing=60.0, + offset=5.0, + margin=0.0, ) + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" + ] + x_values = [point.x for carrier in carriers for point in carrier.Points] + z_values = [point.z for carrier in carriers for point in carrier.Points] - first_kind = getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "") - self.assertEqual("WireDuct", first_kind) + self.assertEqual(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual(4, len(carriers)) + self.assertEqual(60.0, max(x_values)) + self.assertEqual(60.0, max(z_values)) - def test_terminal_access_prefers_wire_duct_over_nearer_routing_range(self): + def test_auto_detect_support_surface_removes_carriers_and_obstacle_mode_when_source_invalid(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", - ) - routing_network.create_route_carrier( + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "安装板A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( doc, - [app.Vector(0, 10, 20), app.Vector(120, 10, 20)], project_uuid="project-1", - kind="WireDuct", + spacing=60.0, + offset=5.0, + margin=0.0, ) - - created = routing_network.create_terminal_access_carriers_from_document( + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 120, 0, 120)) + created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", + spacing=60.0, + offset=5.0, + margin=0.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)) + self.assertEqual(6, len(created)) + self.assertEqual(0, len(created_again)) + self.assertEqual([], routing_network.collect_route_carriers(doc)) + self.assertEqual("", getattr(panel, "QetRoutingObstacleMode", "")) + self.assertEqual("", getattr(panel, "QetRouteCarrierNamesJson", "")) - def test_eplan_connection_route_enters_network_at_segment_projection(self): + def test_eplan_connection_route_can_use_auto_detected_support_surface(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( + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 10, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 10, 0)) + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "Mounting Plate A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) + + created = routing_network.create_surface_carriers_from_document( doc, - [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], project_uuid="project-1", - kind="WireDuct", + spacing=60.0, + offset=5.0, + margin=0.0, ) - 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.assertGreater(len(created), 0) + self.assertEqual("network-dijkstra-v1", result["algorithm"]) + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + self.assertTrue(any(point.y == 10.0 for point in result["points"])) - def test_generate_routing_path_network_adds_wiring_cut_out_carrier(self): + def test_prepare_layout_space_auto_detects_support_surface_sources(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)) + panel = doc.addObject("Part::Feature", "MountingPlateA") + panel.Label = "Mounting Plate A" + panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) - 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" - ] + result = auto_routing_panel.AutoRoutingController().generate_layout_space() - self.assertEqual(1, result["wiring_cut_out_carriers"]) - self.assertEqual(1, len(cut_out_carriers)) - self.assertEqual("PassThrough", cut_out.QetRoutingObstacleMode) + self.assertGreater(result["support_surface_sources"], 0) + self.assertEqual("document", result["source_mode"]) - def test_generate_routing_path_network_refreshes_wiring_cut_out_geometry(self): + def test_generate_routing_paths_uses_selected_wire_duct_entity(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") - 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)) - 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" - ] + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() - 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(1, result["wire_duct_carriers"]) + self.assertEqual("selection", result["source_mode"]) - def test_wiring_cut_out_source_bridge_extension_controls_generated_path_length(self): + def test_generate_routing_paths_uses_selected_route_path_as_user_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") - 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 - - 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.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]) + route_path = doc.addObject("Part::Feature", "UserRouteSketch") + route_path.Label = "用户主路径A" + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[ + FakeEdge(app.Vector(0, 0, 20), app.Vector(0, 80, 20)), + FakeEdge(app.Vector(0, 80, 20), app.Vector(100, 80, 20)), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) - def test_wiring_cut_out_bridges_nearby_ducts_on_both_sides_of_panel(self): + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = routing_network.collect_route_carriers(doc) + user_paths = [item for item in carriers if item.QetRouteCarrierKind == "UserPath"] + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(1, len(user_paths)) + self.assertEqual("UserRouteSketch", user_paths[0].QetRouteSourceName) + self.assertEqual("用户主路径A", user_paths[0].QetRouteSourceLabel) + self.assertEqual("selection", result["source_mode"]) + + def test_controller_creates_selected_user_paths_without_full_network_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") - 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( + 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)], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(1, result["network"]["kinds"]["UserPath"]) + self.assertEqual(1, len(carriers)) + self.assertEqual("UserPath", carriers[0].QetRouteCarrierKind) + + def test_controller_creates_user_path_bridge_from_selected_route_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 = routing_network.create_route_carrier( doc, - [app.Vector(0, -20, 20), app.Vector(50, -20, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", + label="线槽", ) - routing_network.create_route_carrier( + main_path = routing_network.create_route_carrier( doc, - [app.Vector(50, 20, 20), app.Vector(100, 20, 20)], + [app.Vector(120, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", - kind="WireDuct", + kind="RoutingRange", + label="主网络", + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=duct), FakeSelectionItem(obj=main_path)], ) - 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().create_user_path_bridge_from_selection() - self.assertEqual("Routed", result["route_status"]) - self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"]) - self.assertEqual(0, result["collision_count"]) + self.assertEqual(1, result["user_path_bridges"]) + self.assertEqual(1, result["network"]["kinds"]["UserPath"]) - def test_check_routing_path_network_writes_diagnostic_for_unconnected_terminal(self): + def test_controller_creates_user_path_bridge_from_path_network_diagnostic_suggestion(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)], + [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(1000, 0, 0), app.Vector(1000, 100, 0)], + project_uuid="project-1", + kind="TerminalAccess", + label="端子接入", ) - 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) + result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() + carriers = routing_network.collect_route_carriers(doc) + user_paths = [carrier for carrier in carriers if carrier.QetRouteCarrierKind == "UserPath"] - self.assertFalse(result["ok"]) - self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) - 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) + self.assertEqual(1, result["user_path_bridges"]) + self.assertEqual(1, result["diagnostic_suggestions"]) + self.assertEqual(1, len(user_paths)) + self.assertEqual( + [(100.0, 0.0, 0.0), (1000.0, 0.0, 0.0)], + [(point.x, point.y, point.z) for point in user_paths[0].Points], + ) - def test_check_routing_path_network_warns_for_long_terminal_access(self): + def test_controller_creates_user_path_bridge_from_main_path_detour_pair(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, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) - routing_network.create_route_carrier( + routed_group = wiring_objects.ensure_routed_group(doc, "project-1") + fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") + fallback_source.QetRouteSourceLabel = "门板布线面" + current_source = doc.addObject("Part::Feature", "MainUserPathSource") + current_source.QetRouteSourceLabel = "主路径A" + fallback_carrier = routing_network.create_route_carrier( doc, - [app.Vector(900, 0, 20), app.Vector(1000, 0, 20)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", - kind="WireDuct", + kind="RoutingRange", + label="门板布线面 carrier", ) - routing_network.create_terminal_access_carriers_from_document( + current_carrier = routing_network.create_route_carrier( doc, + [app.Vector(140, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=1000.0, + kind="UserPath", + label="主路径A carrier", + ) + fallback_carrier.QetRouteSourceName = fallback_source.Name + fallback_carrier.QetRouteSourceLabel = "门板布线面" + current_carrier.QetRouteSourceName = current_source.Name + current_carrier.QetRouteSourceLabel = "主路径A" + wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_pair_bridge") + wire.Label = "N-PAIR-BRIDGE: A1 -> B1" + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-main-path-pair-bridge" + wire.QetRouteIssueCodes = "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( + { + "segments": [ + {"carrier": {"kind": "UserPath", "source_label": "主路径A"}} + ] + }, + ensure_ascii=False, ) + routed_group.addObject(wire) - result = auto_routing.check_eplan_routing_path_network( - doc, - project_uuid="project-1", - options={"terminal_access_max_distance": 1000.0}, + result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() + user_paths = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] + created_bridges = [ + carrier + for carrier in user_paths + if "QET User Bridge" in str(getattr(carrier, "Label", "") or "") + ] + + self.assertEqual(1, result["main_path_detour_bridge_pairs"]) + self.assertEqual(1, result["main_path_detour_user_path_bridges"]) + self.assertEqual(0, result["main_path_detour_bridge_duplicates"]) + self.assertEqual(1, len(created_bridges)) + self.assertEqual("门板布线面 -> 主路径A", created_bridges[0].QetRouteBridgePairLabel) + self.assertEqual( + [(100.0, 0.0, 0.0), (140.0, 20.0, 0.0)], + [(point.x, point.y, point.z) for point in created_bridges[0].Points], ) - 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"])) - self.assertEqual("terminal-long-access", payload["long_terminal_accesses"][0]["terminal_uuid"]) - self.assertEqual(900.0, payload["long_terminal_accesses"][0]["terminal_access_length_mm"]) - self.assertIn("端子接入过长", message) - self.assertIn("TerminalLongAccess", message) - self.assertIn("terminal-long-access", message) - self.assertIn("900.0 mm", message) + second = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() - def test_check_routing_path_network_warns_for_invalid_terminal_local_route_points(self): + self.assertEqual(0, second["main_path_detour_user_path_bridges"]) + self.assertEqual(1, second["main_path_detour_bridge_duplicates"]) + + def test_controller_does_not_duplicate_diagnostic_user_path_bridge(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 = _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)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) - routing_network.create_terminal_access_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=1000.0, + kind="TerminalAccess", ) + controller = auto_routing_panel.AutoRoutingController() - 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 = controller.create_user_path_bridges_from_diagnostic_suggestions() + second = controller.create_user_path_bridges_from_diagnostic_suggestions() + user_paths = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] - 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.assertIn("端子局部路径无效", message) - self.assertIn("terminal-invalid-local-path", message) + self.assertEqual(1, first["user_path_bridges"]) + self.assertEqual(0, second["user_path_bridges"]) + self.assertEqual(0, second["diagnostic_suggestions"]) + self.assertEqual(1, len(user_paths)) - def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): + def test_route_eplan_connections_auto_creates_diagnostic_user_path_bridge(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") - 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(1000, 0, 0), app.Vector(1100, 0, 0)], + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", + label="孤立线槽", ) - created = routing_network.create_terminal_access_carriers_from_document( + routing_network.create_route_carrier( doc, + [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", - terminal_exit_length=20.0, - max_distance=100.0, + kind="TerminalAccess", + label="端子接入", ) - result = auto_routing.check_eplan_routing_path_network( + report = auto_routing.route_eplan_connections( doc, + payload={"project_uuid": "project-1", "wires": []}, + options={"auto_create_diagnostic_bridges": True}, project_uuid="project-1", - options={"terminal_access_max_distance": 100.0}, + update_network=False, ) + message = auto_routing.format_eplan_connection_route_report(report) + user_paths = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] - self.assertEqual([], created) - self.assertEqual([], result["diagnostic"]["unconnected_terminals"]) + self.assertEqual(1, report["auto_diagnostic_bridges"]["created_count"]) + self.assertEqual(1, len(user_paths)) self.assertNotIn( - "unconnected_terminals", - [issue.get("code") for issue in result["diagnostic"]["issues"]], + "wire_ducts_without_terminal_access", + report["routing_path_network_diagnostic"]["issue_codes"], ) + self.assertIn("自动诊断桥接:生成 UserPath 1 条", message) - 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) - - def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): + def test_route_eplan_connections_does_not_auto_create_diagnostic_bridge_by_default(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") routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="线槽A", + [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(1000, 0, 0), app.Vector(1000, 100, 0)], + project_uuid="project-1", + kind="TerminalAccess", + label="端子接入", ) - result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") - message = auto_routing.format_routing_path_network_report(result["diagnostic"]) + report = auto_routing.route_eplan_connections( + doc, + payload={"project_uuid": "project-1", "wires": []}, + project_uuid="project-1", + update_network=False, + ) + user_paths = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] - self.assertIn("线槽端点疑似断开", message) - self.assertIn("线槽A", message) - self.assertIn("(0.0, 0.0, 20.0)", message) - self.assertIn("补齐相邻线槽", message) + self.assertFalse(report["auto_diagnostic_bridges"]["enabled"]) + self.assertEqual(0, report["auto_diagnostic_bridges"]["created_count"]) + self.assertEqual(0, len(user_paths)) - def test_check_routing_path_network_warns_when_network_is_empty(self): + def test_controller_repeats_diagnostic_bridge_until_no_new_bridge_is_created(self): _install_fake_freecad() - terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") + 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") + original_check = auto_routing.check_eplan_routing_path_network + original_create = routing_network.create_user_path_bridges_from_diagnostic_suggestions + calls = {"check": 0, "create": 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"]) + def fake_check(_doc, project_uuid="", options=None): + calls["check"] += 1 + return {"diagnostic": {"pass_index": calls["check"]}} - self.assertFalse(result["ok"]) - self.assertEqual("empty_routing_path_network", payload["issues"][0]["code"]) - self.assertEqual(0, payload["summary"]["segments"]) - self.assertIn("布线路径网络为空", message) + def fake_create(_doc, diagnostic, project_uuid=""): + calls["create"] += 1 + if int(diagnostic.get("pass_index", 0) or 0) <= 2: + return {"suggestions": 1, "created": [object()], "duplicates": 0, "stale_suggestions": 0} + return {"suggestions": 0, "created": [], "duplicates": 0, "stale_suggestions": 0} - def test_check_routing_path_network_warns_for_invalid_route_carrier_geometry(self): + try: + auto_routing.check_eplan_routing_path_network = fake_check + routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create + + result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() + finally: + auto_routing.check_eplan_routing_path_network = original_check + routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create + + self.assertEqual(2, result["user_path_bridges"]) + self.assertEqual(2, result["diagnostic_suggestions"]) + self.assertEqual(3, result["diagnostic_passes"]) + self.assertEqual(3, calls["check"]) + self.assertEqual(3, calls["create"]) + + def test_selected_curve_edges_are_discretized_as_user_path_polyline(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", + route_path = doc.addObject("Sketcher::SketchObject", "CurvedRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 60, 20, 20), + edges=[ + FakeCurveEdge( + [ + app.Vector(0, 0, 20), + app.Vector(25, 40, 20), + app.Vector(75, 40, 20), + app.Vector(100, 0, 20), + ] + ) + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - carrier.Points = [app.Vector(0, 0, 20)] - 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().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - 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, result["user_path_carriers"]) + self.assertEqual( + [(0.0, 0.0, 20.0), (25.0, 40.0, 20.0), (75.0, 40.0, 20.0), (100.0, 0.0, 20.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) - def test_check_routing_path_network_warns_when_only_routing_range_is_available(self): + def test_selected_shape_wires_are_used_as_user_path_polyline(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") - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="RoutingRange", + route_path = doc.addObject("Sketcher::SketchObject", "WireRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire( + [ + app.Vector(0, 0, 20), + app.Vector(0, 60, 20), + app.Vector(60, 80, 20), + app.Vector(120, 80, 20), + ] + ) + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - 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().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - self.assertFalse(result["ok"]) - self.assertEqual(1, payload["routing_range_only_network"]["routing_range_carriers"]) + self.assertEqual(1, result["user_path_carriers"]) self.assertEqual( - 0, - payload["routing_range_only_network"]["primary_route_carriers"], + [(0.0, 0.0, 20.0), (0.0, 60.0, 20.0), (60.0, 80.0, 20.0), (120.0, 80.0, 20.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], ) - 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): + def test_selected_user_path_shape_points_honor_object_placement(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, - } + 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", "MovedRouteSketch") + route_path.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) + route_path.Shape = FakeShape( + FakeBoundBox(0, 50, 0, 50, 20, 20), + wires=[ + FakeWire( + [ + app.Vector(0, 0, 20), + app.Vector(50, 0, 20), + app.Vector(50, 50, 20), + ] + ) + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) - message = auto_routing.format_routing_path_network_report(diagnostic) + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - self.assertIn("桥接 1 段相邻/投影主路径", message) + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual( + [(100.0, 10.0, 25.0), (150.0, 10.0, 25.0), (150.0, 60.0, 25.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) - def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self): + def test_disconnected_shape_wires_create_separate_user_paths(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)]), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(2, result["user_path_carriers"]) + self.assertEqual(2, len(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 carriers], + ) + self.assertEqual(["1", "2"], [carrier.QetRouteSourcePathIndex for carrier in carriers]) + self.assertEqual(2, len(json.loads(route_path.QetRouteCarrierNamesJson))) + + def test_refreshing_multi_wire_user_path_removes_stale_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") - 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", - ) + route_path = doc.addObject("Sketcher::SketchObject", "EditableMultiWireRouteSketch") + 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)] + first = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", + ) + route_path.Shape = FakeShape( + FakeBoundBox(0, 60, 0, 20, 20, 20), + wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(60, 0, 20)])], + ) - result = auto_routing.check_eplan_routing_path_network( + second = routing_network.create_user_path_carriers_from_selection( doc, + selection, project_uuid="project-1", - options={"adjoining_duct_tolerance": 15.0}, ) + carriers = routing_network.collect_route_carriers(doc) - self.assertTrue(result["ok"]) - self.assertEqual(1, result["diagnostic"]["summary"]["bridged_segments"]) - self.assertEqual([], result["diagnostic"]["possible_breaks"]) + self.assertEqual(2, len(first)) + self.assertEqual(1, len(second)) + self.assertEqual(1, len(carriers)) + self.assertEqual([(0.0, 0.0, 20.0), (60.0, 0.0, 20.0)], [(p.x, p.y, p.z) for p in carriers[0].Points]) + self.assertEqual("", carriers[0].QetRouteSourcePathIndex) + self.assertEqual(1, len(json.loads(route_path.QetRouteCarrierNamesJson))) - def test_generate_routing_path_network_skips_far_terminal_access_to_protect_view_bbox(self): + def test_refreshing_single_wire_user_path_adds_new_carriers_for_added_wires(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)) + route_path = doc.addObject("Sketcher::SketchObject", "GrowingMultiWireRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 60, 0, 20, 20, 20), + wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(60, 0, 20)])], + ) + selection = [FakeSelectionItem(obj=route_path)] + first = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", + ) + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire([app.Vector(0, 0, 20), app.Vector(60, 0, 20)]), + FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), + ], + ) - result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + second = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", + ) + 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(0, result["terminal_access_carriers"]) + self.assertEqual(1, len(first)) + self.assertEqual(2, len(second)) + self.assertEqual(2, len(carriers)) + self.assertEqual(["1", "2"], [carrier.QetRouteSourcePathIndex for carrier in carriers]) + self.assertEqual(2, len(json.loads(route_path.QetRouteCarrierNamesJson))) - def test_auto_routing_controller_exposes_terminal_access_max_distance(self): + def test_controller_marks_selected_object_as_cabinet_interior_boundary(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") - _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)) + cabinet_space = doc.addObject("Part::Feature", "CabinetInteriorSpace") + cabinet_space.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=cabinet_space)], + ) - controller = auto_routing_panel.AutoRoutingController() - controller.set_terminal_access_max_distance(6000.0) - result = controller.generate_routing_paths() + result = auto_routing_panel.AutoRoutingController().mark_cabinet_boundary_from_selection() - self.assertEqual(1, result["terminal_access_carriers"]) - self.assertEqual(6000.0, controller.routing_options()["terminal_access_max_distance"]) + self.assertEqual(1, result["cabinet_boundary_objects"]) + self.assertEqual("CabinetInterior", cabinet_space.QetRoutingBoundaryKind) + self.assertEqual("RoutingBoundary", cabinet_space.QetRoutingRole) + self.assertEqual("PassThrough", cabinet_space.QetRoutingObstacleMode) + self.assertEqual(1, len(auto_routing.collect_routing_boundaries(doc))) - def test_auto_routing_controller_exposes_terminal_exit_length(self): + def test_controller_marks_selected_object_obstacle_pass_through_and_restores(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") - _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)) - + obstacle = doc.addObject("Part::Feature", "BracketObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=obstacle)], + ) 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")), - ) - self.assertEqual(40.0, controller.routing_options()["terminal_exit_length"]) + ignored = controller.mark_selected_objects_pass_through_obstacle() + self.assertEqual([], auto_routing.collect_obstacles(doc)) + restored = controller.restore_selected_objects_as_obstacles() - def test_route_eplan_connections_prepares_layout_space_like_eplan_route(self): + self.assertEqual(1, ignored["obstacle_mode_objects"]) + self.assertEqual(1, restored["obstacle_mode_objects"]) + self.assertEqual("", obstacle.QetRoutingObstacleMode) + self.assertEqual(["BracketObstacle"], [item["name"] for item in auto_routing.collect_obstacles(doc)]) + + def test_controller_summary_reports_pass_through_obstacle_count(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -2387,2471 +3033,11734 @@ class AutoRoutingTest(unittest.TestCase): 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", - } - ], - } + bracket = doc.addObject("Part::Feature", "BracketObstacle") + bracket.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) + bracket.QetRoutingObstacleMode = "PassThrough" - report = auto_routing_panel.AutoRoutingController().route_eplan_connections() + message = auto_routing_panel.AutoRoutingController().summary() - 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.assertIn("忽略碰撞:1", message) - def test_auto_routing_controller_passes_adjoining_duct_tolerance_to_batch_route(self): + def test_controller_selects_top_collision_obstacles_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") - _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", + obstacle = doc.addObject("Part::Feature", "Compound053") + obstacle.Label = "NAUO118" + other = doc.addObject("Part::Feature", "Compound039") + other.Label = "NAUO141" + 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( + { + "top_collision_obstacles": [ + {"label": "NAUO118", "name": "Compound053", "count": 18}, + {"label": "NAUO141", "name": "Compound039", "count": 6}, + {"label": "NAUO404", "name": "Compound404", "count": 1}, + ] + }, + ensure_ascii=False, ) - routing_network.create_route_carrier( - doc, - [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", + 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: [], ) - 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_panel.AutoRoutingController().select_top_collision_obstacles() - self.assertEqual(1, report["routed"]) - self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) + self.assertEqual(2, result["selected_collision_obstacles"]) + self.assertEqual(["Compound053", "Compound039"], result["selected_collision_obstacle_names"]) + self.assertEqual(["Compound404"], result["missing_collision_obstacle_names"]) + self.assertEqual([obstacle, other], selected) - def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(self): + def test_controller_selects_top_collision_parent_assemblies_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") - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], - project_uuid="project-1", - kind="WireDuct", + door = doc.addObject("App::LinkGroup", "DoorAssembly") + door.Label = "FRONT DOOR-R ASS'Y" + cabinet = doc.addObject("App::LinkGroup", "CabinetAssembly") + cabinet.Label = "CABINET ASS'Y" + 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( + { + "top_collision_obstacles": [ + { + "label": "NAUO141", + "name": "Compound039", + "count": 6, + "parent_names": ["DoorAssembly"], + "parent_labels": ["FRONT DOOR-R ASS'Y"], + }, + { + "label": "NAUO142", + "name": "Compound040", + "count": 3, + "parent_names": ["DoorAssembly"], + "parent_labels": ["FRONT DOOR-R ASS'Y"], + }, + { + "label": "NAUO118", + "name": "Compound053", + "count": 18, + "parent_names": ["CabinetAssembly"], + "parent_labels": ["CABINET ASS'Y"], + }, + { + "label": "NAUO404", + "name": "Compound404", + "count": 1, + "parent_names": ["MissingAssembly"], + "parent_labels": ["MISSING ASS'Y"], + }, + ] + }, + ensure_ascii=False, ) - routing_network.create_route_carrier( - doc, - [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", + 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: [], ) - summary = auto_routing_panel.AutoRoutingController( - options={"adjoining_duct_tolerance": 15.0} - ).summary() + result = auto_routing_panel.AutoRoutingController().select_top_collision_parent_assemblies() - self.assertIn("桥接:1", summary) + self.assertEqual(2, result["selected_collision_parent_assemblies"]) + self.assertEqual(["DoorAssembly", "CabinetAssembly"], result["selected_collision_parent_assembly_names"]) + self.assertEqual(["MissingAssembly"], result["missing_collision_parent_assembly_refs"]) + self.assertEqual([door, cabinet], selected) - def test_auto_routing_controller_exposes_lane_spacing(self): + def test_controller_selects_only_structural_collision_parent_assemblies(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") - _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)], - project_uuid="project-1", - kind="WireDuct", + door = doc.addObject("App::LinkGroup", "DoorAssembly") + door.Label = "FRONT DOOR-R ASS'Y" + device_assembly = doc.addObject("App::LinkGroup", "DeviceAssembly") + device_assembly.Label = "DEVICE ASS'Y" + 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( + { + "top_collision_obstacles": [ + { + "label": "NAUO141", + "name": "Compound039", + "count": 6, + "parent_names": ["DoorAssembly"], + "parent_labels": ["FRONT DOOR-R ASS'Y"], + "resolution_hint_code": "review_pass_through_structural_obstacle", + }, + { + "label": "ID:12", + "name": "QETDevice_A", + "count": 3, + "parent_names": ["DeviceAssembly"], + "parent_labels": ["DEVICE ASS'Y"], + "resolution_hint_code": "review_device_or_layout_collision", + }, + { + "label": "支架缺失", + "name": "MissingBracket", + "count": 1, + "parent_names": ["MissingStructure"], + "parent_labels": ["MISSING STRUCTURE"], + "resolution_hint_code": "review_pass_through_structural_obstacle", + }, + ] + }, + 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: [], ) - 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_lane_spacing(14.0) - report = controller.route_eplan_connections() + result = auto_routing_panel.AutoRoutingController().select_structural_collision_parent_assemblies() - 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.assertEqual(1, result["selected_structural_collision_parent_assemblies"]) + self.assertEqual(["DoorAssembly"], result["selected_structural_collision_parent_assembly_names"]) + self.assertEqual(["MissingStructure"], result["missing_structural_collision_parent_assembly_refs"]) + self.assertEqual([door], selected) + self.assertNotIn(device_assembly, selected) - def test_auto_routing_controller_exposes_lane_axis(self): + def test_controller_marks_only_structural_collision_parent_assemblies_pass_through(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], - project_uuid="project-1", - kind="WireDuct", + door = doc.addObject("App::LinkGroup", "DoorAssembly") + door.Label = "FRONT DOOR-R ASS'Y" + device_assembly = doc.addObject("App::LinkGroup", "DeviceAssembly") + device_assembly.Label = "DEVICE ASS'Y" + 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( + { + "top_collision_obstacles": [ + { + "label": "NAUO141", + "name": "Compound039", + "count": 6, + "parent_names": ["DoorAssembly"], + "parent_labels": ["FRONT DOOR-R ASS'Y"], + "resolution_hint_code": "review_pass_through_structural_obstacle", + }, + { + "label": "ID:12", + "name": "QETDevice_A", + "count": 3, + "parent_names": ["DeviceAssembly"], + "parent_labels": ["DEVICE ASS'Y"], + "resolution_hint_code": "review_device_or_layout_collision", + }, + { + "label": "支架缺失", + "name": "MissingBracket", + "count": 1, + "parent_names": ["MissingStructure"], + "parent_labels": ["MISSING STRUCTURE"], + "resolution_hint_code": "review_pass_through_structural_obstacle", + }, + ] + }, + ensure_ascii=False, ) - 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", - }, - ], - } + diagnostic_group.addObject(diagnostic) - controller = auto_routing_panel.AutoRoutingController() - controller.set_lane_spacing(8.0) - controller.set_lane_axis("z") - report = controller.route_eplan_connections() + result = auto_routing_panel.AutoRoutingController().mark_structural_collision_parent_assemblies_pass_through() - 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.assertEqual(1, result["marked_structural_collision_parent_assemblies"]) + self.assertEqual(["DoorAssembly"], result["marked_structural_collision_parent_assembly_names"]) + self.assertEqual(["MissingStructure"], result["missing_structural_collision_parent_assembly_refs"]) + self.assertEqual("PassThrough", door.QetRoutingObstacleMode) + self.assertEqual("", getattr(device_assembly, "QetRoutingObstacleMode", "")) - def test_auto_routing_controller_exposes_lane_max_offset(self): + def test_controller_marks_nearest_structural_parent_without_broad_root_group(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)], - project_uuid="project-1", - kind="WireDuct", + door = doc.addObject("App::LinkGroup", "DoorAssembly") + door.Label = "FRONT DOOR-R ASS'Y" + cabinet = doc.addObject("App::LinkGroup", "CabinetAssembly") + cabinet.Label = "MCCB CABINET ASS'Y" + project_root = doc.addObject("App::DocumentObjectGroup", "QETExchangeDevices") + project_root.Label = "QET Exchange Devices" + 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( + { + "top_collision_obstacles": [ + { + "label": "NAUO141", + "name": "Compound039", + "count": 6, + "parent_names": [ + "DoorAssembly", + "CabinetAssembly", + "QETExchangeDevices", + ], + "parent_labels": [ + "FRONT DOOR-R ASS'Y", + "MCCB CABINET ASS'Y", + "QET Exchange Devices", + ], + "resolution_hint_code": "review_pass_through_structural_obstacle", + }, + ] + }, + ensure_ascii=False, ) + diagnostic_group.addObject(diagnostic) - 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, - route_index=21, - options=controller.routing_options(), - ) + result = auto_routing_panel.AutoRoutingController().mark_structural_collision_parent_assemblies_pass_through() - 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"]) + self.assertEqual(1, result["marked_structural_collision_parent_assemblies"]) + self.assertEqual(["DoorAssembly"], result["marked_structural_collision_parent_assembly_names"]) + self.assertEqual("PassThrough", door.QetRoutingObstacleMode) + self.assertEqual("", getattr(cabinet, "QetRoutingObstacleMode", "")) + self.assertEqual("", getattr(project_root, "QetRoutingObstacleMode", "")) - def test_auto_routing_panel_command_button_style_keeps_text_visible(self): + def test_controller_selects_only_device_or_layout_collision_obstacles(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"] + 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, 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", + device = doc.addObject("Part::Feature", "QETDevice_A") + device.Label = "ID:12" + bracket = doc.addObject("Part::Feature", "Bracket_A") + bracket.Label = "NAUO141" + 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( + { + "top_collision_obstacles": [ + { + "label": "ID:12", + "name": "QETDevice_A", + "count": 2, + "resolution_hint_code": "review_device_or_layout_collision", + }, + { + "label": "NAUO141", + "name": "Bracket_A", + "count": 6, + "resolution_hint_code": "review_pass_through_structural_obstacle", + }, + { + "label": "缺失设备", + "name": "MissingDevice", + "count": 1, + "resolution_hint_code": "review_device_or_layout_collision", + }, + ] + }, + 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: [], ) - with self.assertRaises(auto_routing.AutoRoutingError): - auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().select_device_or_layout_collision_obstacles() - def test_route_eplan_connection_between_terminals_fails_without_network(self): + self.assertEqual(1, result["selected_device_or_layout_collision_obstacles"]) + self.assertEqual(["QETDevice_A"], result["selected_device_or_layout_collision_obstacle_names"]) + self.assertEqual(["MissingDevice"], result["missing_device_or_layout_collision_obstacle_names"]) + self.assertEqual([device], selected) + self.assertNotIn(bracket, selected) + + def test_controller_selects_collision_wires_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") - 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)) + wire_a = doc.addObject("Part::Feature", "QETRoutedConnection_wire_a") + wire_a.Label = "N-COL-A: A1 -> B1 (CollisionWarning)" + wire_a.RouteType = "RoutedConnection" + wire_a.QetWireUuid = "wire-a" + wire_b = doc.addObject("Part::Feature", "QETRoutedConnection_wire_b") + wire_b.Label = "N-COL-B: A2 -> B2 (CollisionWarning)" + wire_b.RouteType = "RoutedConnection" + wire_b.QetWireUuid = "wire-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( + { + "collision_samples": [ + { + "wire_uuid": "wire-a", + "wire_object_label": "N-COL-A: A1 -> B1 (CollisionWarning)", + }, + { + "wire_uuid": "wire-missing", + "wire_object_label": "N-MISSING: A3 -> B3 (CollisionWarning)", + }, + ], + "route_samples": [ + { + "wire_uuid": "wire-b", + "wire_object_label": "N-COL-B: A2 -> B2 (CollisionWarning)", + "issue_codes": ["collision_warnings"], + } + ], + }, + 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: [], + ) - 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))) + result = auto_routing_panel.AutoRoutingController().select_collision_wires() - def test_surface_carrier_grid_uses_actual_rotated_face_plane(self): + self.assertEqual(2, result["selected_collision_wires"]) + self.assertEqual( + ["QETRoutedConnection_wire_a", "QETRoutedConnection_wire_b"], + result["selected_collision_wire_names"], + ) + self.assertEqual(["wire-missing"], result["missing_collision_wire_refs"]) + self.assertEqual([wire_a, wire_b], selected) + + def test_controller_selects_collision_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") - 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), + collision_wire = doc.addObject("Part::Feature", "QETRoutedConnection_third_party") + collision_wire.Label = "N-COL: A1 -> B1 (CollisionWarning)" + collision_wire.RouteType = "RoutedConnection" + collision_wire.QetWireUuid = "wire-third-party" + collision_wire.QetRouteIssueCodes = "collision_warnings, third_party_device_collisions" + normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") + normal_wire.Label = "N-OK: A2 -> B2 (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( + {"route_samples": []}, + 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: [], ) - created = routing_network.create_surface_carriers_from_selection( - doc, - [FakeSelectionItem([face])], - project_uuid="project-1", - spacing=50.0, - offset=10.0, - margin=0.0, - ) + result = auto_routing_panel.AutoRoutingController().select_collision_wires() - 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) + self.assertEqual(1, result["selected_collision_wires"]) + self.assertEqual( + ["QETRoutedConnection_third_party"], + result["selected_collision_wire_names"], + ) + self.assertEqual([], result["missing_collision_wire_refs"]) + self.assertEqual([collision_wire], selected) - def test_route_path_creation_ignores_whole_solid_object_edges(self): + def test_controller_selects_issue_wires_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") - 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()], + long_wire = doc.addObject("Part::Feature", "QETRoutedConnection_long") + long_wire.Label = "N-LONG: A1 -> B1 (Routed)" + long_wire.RouteType = "RoutedConnection" + long_wire.QetWireUuid = "wire-long" + boundary_wire = doc.addObject("Part::Feature", "QETRoutedConnection_boundary") + boundary_wire.Label = "N-BOUNDARY: A2 -> B2 (BoundaryWarning)" + boundary_wire.RouteType = "RoutedConnection" + boundary_wire.QetWireUuid = "wire-boundary" + 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" + 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": [ + { + "wire_uuid": "wire-long", + "wire_object_label": "N-LONG: A1 -> B1 (Routed)", + "issue_codes": ["long_terminal_access"], + }, + { + "wire_uuid": "wire-boundary", + "wire_object_label": "N-BOUNDARY: A2 -> B2 (BoundaryWarning)", + "issue_codes": ["boundary_warning"], + }, + { + "wire_uuid": "wire-ok", + "wire_object_label": "N-OK: A3 -> B3 (Routed)", + "issue_codes": [], + }, + { + "wire_uuid": "wire-missing", + "wire_object_label": "N-MISSING: A4 -> B4 (Routed)", + "issue_codes": ["route_capacity_pressure"], + }, + ] + }, + ensure_ascii=False, ) - - created = routing_network.create_carriers_from_selection( - doc, - [FakeSelectionItem(obj=solid)], - 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: [], ) - self.assertEqual([], created) + result = auto_routing_panel.AutoRoutingController().select_issue_wires() - def test_route_path_creation_projects_line_to_selected_face(self): + self.assertEqual(2, result["selected_issue_wires"]) + self.assertEqual( + ["QETRoutedConnection_long", "QETRoutedConnection_boundary"], + result["selected_issue_wire_names"], + ) + self.assertEqual(["wire-missing"], result["missing_issue_wire_refs"]) + self.assertEqual([long_wire, boundary_wire], selected) + + def test_controller_selects_main_path_detour_missing_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() + 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") - face = FakeFace( - FakeBoundBox(0, 100, 0, 100, 0, 0), - app.Vector(0, 0, 1), + sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled") + sampled_wire.Label = "N-MAINPATH-A: A1 -> B1 (MainPathDetourMissing)" + sampled_wire.RouteType = "RoutedConnection" + sampled_wire.QetWireUuid = "wire-sampled" + object_issue_wire = doc.addObject("Part::Feature", "QETRoutedConnection_object_issue") + object_issue_wire.Label = "N-MAINPATH-B: A2 -> B2 (MainPathDetourMissing)" + object_issue_wire.RouteType = "RoutedConnection" + object_issue_wire.QetWireUuid = "wire-object-issue" + object_issue_wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + 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 = "collision_warnings" + 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": [ + { + "wire_uuid": "wire-sampled", + "wire_object_label": "N-MAINPATH-A: A1 -> B1 (MainPathDetourMissing)", + "issue_codes": ["main_path_detour_missing"], + }, + { + "wire_uuid": "wire-missing", + "wire_object_label": "N-MISSING: A4 -> B4 (MainPathDetourMissing)", + "issue_codes": ["main_path_detour_missing"], + }, + { + "wire_uuid": "wire-ok", + "wire_object_label": "N-OK: A3 -> B3 (Routed)", + "issue_codes": ["collision_warnings"], + }, + ] + }, + ensure_ascii=False, ) - 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))], + 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: [], ) - created = routing_network.create_carriers_from_selection( - doc, - [ - FakeSelectionItem([face]), - FakeSelectionItem(obj=draft_line), - ], - project_uuid="project-1", - ) + result = auto_routing_panel.AutoRoutingController().select_main_path_detour_missing_wires() - self.assertEqual(1, len(created)) - self.assertEqual([2.0, 2.0], [point.z for point in created[0].Points]) + self.assertEqual(2, result["selected_main_path_detour_missing_wires"]) + self.assertEqual( + ["QETRoutedConnection_sampled", "QETRoutedConnection_object_issue"], + result["selected_main_path_detour_missing_wire_names"], + ) + self.assertEqual(["wire-missing"], result["missing_main_path_detour_missing_wire_refs"]) + self.assertEqual([sampled_wire, object_issue_wire], selected) - def test_wire_duct_entity_generates_centerline_and_marks_source_pass_through(self): + def test_controller_selects_issue_route_sources_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") - 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( + source_path = doc.addObject("Part::Feature", "YellowMainRouteSketch") + source_path.Label = "黄色主路径" + carrier = routing_network.create_route_carrier( doc, - [FakeSelectionItem(obj=duct)], + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="QET User Route Path 黄色主路径", project_uuid="project-1", - margin=20.0, + kind="UserPath", + ) + carrier.QetRouteSourceName = "YellowMainRouteSketch" + carrier.QetRouteSourceLabel = "黄色主路径" + 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": [ + { + "wire_uuid": "wire-boundary", + "wire_object_label": "N-BOUNDARY: A2 -> B2 (BoundaryWarning)", + "issue_codes": ["boundary_warning"], + "carrier_names": [carrier.Name], + "route_track": { + "segments": [ + { + "carrier": { + "name": carrier.Name, + "label": carrier.Label, + "source_name": "YellowMainRouteSketch", + "source_label": "黄色主路径", + } + } + ] + }, + }, + { + "wire_uuid": "wire-ok", + "wire_object_label": "N-OK: A3 -> B3 (Routed)", + "issue_codes": [], + "carrier_names": ["MissingCarrier"], + }, + ] + }, + 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(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]) + result = auto_routing_panel.AutoRoutingController().select_issue_route_sources() - def test_wire_duct_source_end_margin_controls_generated_centerline_length(self): + self.assertEqual(2, result["selected_issue_route_objects"]) + self.assertEqual([carrier.Name], result["selected_issue_route_carrier_names"]) + self.assertEqual(["YellowMainRouteSketch"], result["selected_issue_route_source_names"]) + self.assertEqual([], result["missing_issue_route_refs"]) + self.assertEqual([carrier, source_path], selected) + + def test_controller_selects_main_path_detour_missing_route_sources_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") - 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( + source_path = doc.addObject("Part::Feature", "YellowMainPathSketch") + source_path.Label = "黄色主路径" + carrier = routing_network.create_route_carrier( doc, + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="QET User Route Path 黄色主路径", project_uuid="project-1", + kind="UserPath", + ) + carrier.QetRouteSourceName = source_path.Name + carrier.QetRouteSourceLabel = source_path.Label + 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": [ + { + "wire_uuid": "wire-main-path", + "wire_object_label": "N-MAINPATH: A1 -> B1 (CollisionWarning)", + "issue_codes": ["collision_warnings", "main_path_detour_missing"], + "carrier_names": [carrier.Name], + "route_track": { + "segments": [ + { + "carrier": { + "name": carrier.Name, + "label": carrier.Label, + "source_name": source_path.Name, + "source_label": source_path.Label, + } + } + ] + }, + }, + { + "wire_uuid": "wire-long", + "wire_object_label": "N-LONG: A2 -> B2 (LongAccessWarning)", + "issue_codes": ["long_terminal_access"], + "carrier_names": ["MissingLongAccessCarrier"], + }, + ] + }, + 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: [], ) - 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]) + result = auto_routing_panel.AutoRoutingController().select_main_path_detour_missing_route_sources() - def test_wire_duct_source_capacity_is_copied_to_generated_carriers(self): + self.assertEqual(2, result["selected_main_path_detour_route_objects"]) + self.assertEqual([carrier.Name], result["selected_main_path_detour_route_carrier_names"]) + self.assertEqual([source_path.Name], result["selected_main_path_detour_route_source_names"]) + self.assertEqual([], result["missing_main_path_detour_route_refs"]) + self.assertEqual([carrier, source_path], selected) + + def test_controller_selects_main_path_detour_route_sources_from_wire_object_track(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") - duct = doc.addObject("Part::Feature", "WireDuct") - duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) - duct.QetRouteCarrierCapacity = 4 - - created = routing_network.create_wire_duct_carriers_from_selection( + source_path = doc.addObject("Part::Feature", "YellowMainPathSketch") + source_path.Label = "黄色主路径" + carrier = routing_network.create_route_carrier( doc, - [FakeSelectionItem(obj=duct)], + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="QET User Route Path 黄色主路径", project_uuid="project-1", - margin=20.0, + kind="UserPath", + ) + carrier.QetRouteSourceName = source_path.Name + carrier.QetRouteSourceLabel = source_path.Label + 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": []}, ensure_ascii=False) + diagnostic_group.addObject(diagnostic) + routed_group = wiring_objects.ensure_routed_group(doc, "project-1") + wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path") + wire.Label = "N-MAINPATH: A1 -> B1 (CollisionWarning)" + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-main-path" + wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteTrackJson"] + wire.QetStartTerminalUuid = "terminal-a" + wire.QetEndTerminalUuid = "terminal-b" + wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + wire.QetRouteTrackJson = json.dumps( + { + "carrier_names": [carrier.Name], + "segments": [ + { + "carrier": { + "name": carrier.Name, + "label": carrier.Label, + "source_name": source_path.Name, + "source_label": source_path.Label, + } + } + ], + }, + ensure_ascii=False, + ) + routed_group.addObject(wire) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - self.assertIn("QetRouteCarrierCapacity", duct.PropertiesList) - self.assertTrue(all(item.QetRouteCarrierCapacity == 4 for item in created)) + result = auto_routing_panel.AutoRoutingController().select_main_path_detour_missing_route_sources() - def test_auto_detect_wire_ducts_ignores_cabinet_models(self): + self.assertEqual(2, result["selected_main_path_detour_route_objects"]) + self.assertEqual([carrier.Name], result["selected_main_path_detour_route_carrier_names"]) + self.assertEqual([source_path.Name], result["selected_main_path_detour_route_source_names"]) + self.assertEqual([], result["missing_main_path_detour_route_refs"]) + self.assertEqual([carrier, source_path], selected) + + def test_controller_selects_route_sources_from_selected_wire_track(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") - 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( + source_path = doc.addObject("Part::Feature", "YellowMainRouteSketch") + source_path.Label = "黄色主路径" + carrier = routing_network.create_route_carrier( doc, + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="QET User Route Path 黄色主路径", project_uuid="project-1", + kind="UserPath", ) - created_again = routing_network.create_wire_duct_carriers_from_document( - doc, - project_uuid="project-1", + routed_wire = doc.addObject("Part::Feature", "QETRoutedConnection_problem") + routed_wire.RouteType = "RoutedConnection" + routed_wire.QetRouteTrackJson = json.dumps( + { + "carrier_names": [carrier.Name], + "segments": [ + { + "carrier": { + "name": carrier.Name, + "label": carrier.Label, + "source_name": source_path.Name, + "source_label": source_path.Label, + } + } + ], + }, + ensure_ascii=False, + ) + selected = [routed_wire] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - 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")) + result = auto_routing_panel.AutoRoutingController().select_selected_wire_route_sources() - def test_wire_duct_source_is_not_reported_as_collision(self): + self.assertEqual(2, result["selected_wire_route_objects"]) + self.assertEqual([carrier.Name], result["selected_wire_route_carrier_names"]) + self.assertEqual([source_path.Name], result["selected_wire_route_source_names"]) + self.assertEqual([carrier, source_path], selected) + + def test_controller_selects_rejected_fallback_sources_from_selected_wire_diagnostics(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, 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( + fallback_source = doc.addObject("Part::Feature", "AuxiliaryRoutingRangeSource") + fallback_source.Label = "辅助面" + routing_network.create_route_carrier( doc, - [FakeSelectionItem(obj=duct)], + [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], + label="QET Routing Range 辅助面", project_uuid="project-1", - margin=0.0, + kind="RoutingRange", + ) + routed_wire = doc.addObject("Part::Feature", "QETRoutedConnection_missing_main_path") + routed_wire.RouteType = "RoutedConnection" + routed_wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + routed_wire.QetRouteDiagnosticsJson = json.dumps( + { + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["RoutingRange"], + "rejected_fallback_labels": ["辅助面", "缺失辅助路径"], + } + }, + ensure_ascii=False, + ) + selected = [routed_wire] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().select_selected_wire_rejected_fallback_sources() - self.assertEqual("network-dijkstra-v1", result["algorithm"]) - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) + self.assertEqual(1, result["selected_rejected_fallback_sources"]) + self.assertEqual([fallback_source.Name], result["selected_rejected_fallback_source_names"]) + self.assertEqual(["辅助面", "缺失辅助路径"], result["rejected_fallback_source_labels"]) + self.assertEqual(["RoutingRange"], result["rejected_fallback_source_kinds"]) + self.assertEqual(["QETRoutedConnection_missing_main_path"], result["selected_rejected_fallback_wire_names"]) + self.assertEqual(["缺失辅助路径"], result["missing_rejected_fallback_source_refs"]) + self.assertEqual([fallback_source], selected) - def test_eplan_connection_route_uses_alternate_carrier_to_avoid_obstacle(self): + def test_controller_selects_main_path_detour_rejected_fallback_sources_from_summary(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, 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", + 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.Label = "QET Routing Range Carrier 001" + fallback_source.QetRouteSourceLabel = "安装板布线面" + fallback_source.QetRouteSourceName = "CabinetBackPlateSketch" + wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path") + wire.Label = "N-MAINPATH: A1 -> B1" + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-main-path" + wire.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + ] + wire.QetStartTerminalUuid = "terminal-a" + wire.QetEndTerminalUuid = "terminal-b" + wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + wire.QetRouteDiagnosticsJson = json.dumps( + { + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["RoutingRange"], + "rejected_fallback_labels": ["安装板布线面", "缺失补路位置"], + } + }, + ensure_ascii=False, ) - 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", + routed_group.addObject(wire) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - 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) + result = auto_routing_panel.AutoRoutingController().select_main_path_detour_rejected_fallback_sources() - 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.assertEqual(1, result["selected_main_path_detour_rejected_fallback_sources"]) + self.assertEqual([fallback_source.Name], result["selected_main_path_detour_rejected_fallback_source_names"]) + self.assertEqual(["安装板布线面", "缺失补路位置"], result["main_path_detour_rejected_fallback_labels"]) + self.assertEqual( + {"安装板布线面": 1, "缺失补路位置": 1}, + result["main_path_detour_rejected_fallback_label_counts"], + ) + self.assertEqual({"RoutingRange": 1}, result["main_path_detour_rejected_fallback_kind_counts"]) + self.assertEqual(["缺失补路位置"], result["missing_main_path_detour_rejected_fallback_refs"]) + self.assertEqual([fallback_source], selected) - def test_eplan_connection_route_prefers_entry_candidate_without_access_collision(self): + 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() + 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, 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", + 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( + { + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["RoutingRange"], + "rejected_fallback_labels": ["安装板布线面"], + } + }, + ensure_ascii=False, ) - 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", + wire.QetRouteTrackJson = json.dumps( + { + "segments": [ + { + "carrier": { + "kind": "WireDuct", + "source_label": "主线槽A", + "source_name": current_source.Name, + } + } + ] + }, + ensure_ascii=False, ) - 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}, + routed_group.addObject(wire) + selected = [] + gui.Selection = types.SimpleNamespace( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], ) - 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"]) + result = auto_routing_panel.AutoRoutingController().select_main_path_detour_rejected_fallback_sources() - def test_eplan_connection_route_chooses_clear_orthogonal_access_order(self): + 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_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") - 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", + 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_RoutingConnectionBatch") + diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticJson = json.dumps( + { + "route_samples": [ + { + "wire_uuid": "wire-sampled", + "wire_object_label": "N-SAMPLED: A1 -> B1 (BoundaryWarning)", + "issue_codes": ["boundary_warning"], + } + ] + }, + ensure_ascii=False, ) - 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}, + 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: [], ) - 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"]) + result = auto_routing_panel.AutoRoutingController().select_issue_wires() - def test_eplan_connection_route_marks_collision_warning_against_obstacle_bbox(self): + 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_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") - 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", + 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: [], ) - obstacle = doc.addObject("Part::Feature", "Obstacle") - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().select_long_terminal_accesses() - 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(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_eplan_connection_route_marks_clearance_warning_against_expanded_obstacle_bbox(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") - 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", + 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, ) - 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}, + 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("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"]) + result = auto_routing_panel.AutoRoutingController().select_long_terminal_access_devices() - def test_eplan_connection_route_ignores_terminal_exit_segment_collision(self): + 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_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") - 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", + 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, + ) + 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: [], ) - terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") - terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) + 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_eplan_connection_route_ignores_endpoint_device_body_as_obstacle(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") - 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", + 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, + ) + 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.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() - self.assertEqual("Routed", result["route_status"]) - self.assertEqual(0, result["collision_count"]) + 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_route_eplan_connections_from_payload_skips_missing_terminal(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") - _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", - } - ] - } + 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, + ) + 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: [], + ) - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() - 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(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_route_eplan_connections_from_payload_skips_resolved_tasks_without_route_network(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") - _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") + 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( + clearSelection=lambda: selected.clear(), + addSelection=lambda obj: selected.append(obj), + getSelection=lambda: list(selected), + getSelectionEx=lambda: [], + ) - 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 + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_counterpart_terminals() - 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)) + 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_route_eplan_connection_tasks_marks_task_missing_route_network_when_skipped(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") - _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", + 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: [], ) - task.RouteStatus = "Routed" - report = auto_routing.route_eplan_connection_tasks(doc) + result = auto_routing_panel.AutoRoutingController().select_missing_terminal_candidate_terminals() - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["skipped_missing_route_network"]) - self.assertEqual("MissingRouteNetwork", task.RouteStatus) + 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_eplan_connection_route_prefers_wire_duct_over_shorter_routing_range(self): + def test_controller_selects_boundary_issue_route_carriers_and_terminals(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, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(300, 0, 0)) - routing_network.create_route_carrier( + route = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(300, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(140, 0, 20)], + label="柜内主路径A", project_uuid="project-1", - kind="RoutingRange", + kind="UserPath", ) - 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", + 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.route_eplan_connection_between_terminals(doc, start, end) + result = auto_routing_panel.AutoRoutingController().select_boundary_issue_route_carriers_and_terminals() - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + 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_eplan_connection_route_prefers_wire_duct_when_routing_range_is_only_moderately_shorter(self): + def test_controller_marks_selected_route_carrier_constraint_modes(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, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(10, 0, 0)) - routing_network.create_route_carrier( + carrier = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="测试路径", project_uuid="project-1", - kind="RoutingRange", + kind="UserPath", ) - 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", + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=carrier)], ) + controller = auto_routing_panel.AutoRoutingController() - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + forbidden = controller.mark_selected_route_carriers_forbidden() + required = controller.mark_selected_route_carriers_required() + cleared = controller.clear_selected_route_carrier_constraints() - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + 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_eplan_connection_route_considers_primary_entry_beyond_nearest_surface_candidates(self): + def test_controller_sets_selected_route_carrier_capacity(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, 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( + 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, 20, 20), app.Vector(100, 20, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="测试路径", project_uuid="project-1", - kind="WireDuct", + kind="UserPath", + ) + routing_network._mark_user_path_source(source, carrier) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=source)], ) + controller = auto_routing_panel.AutoRoutingController(options={"selected_route_capacity": 5}) - result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + report = controller.set_selected_route_carriers_capacity() - self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) - self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) + 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_route_eplan_connections_from_payload_skips_tasks_when_carriers_have_no_segments(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") - _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", + 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))], ) - terminal_objects.ensure_bool_property( - broken_carrier, - "CanRouteWire", - "QET Routing", - "Whether routing connections can use this path", - True, + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - 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}, - ) + report = auto_routing_panel.AutoRoutingController().mark_selected_route_carriers_required() - 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(0, report["route_constraint_carriers"]) + self.assertEqual(1, report["route_constraint_sources"]) + self.assertEqual("Required", route_path.QetRouteConstraintMode) - def test_route_eplan_connections_classifies_disconnected_network_as_missing_route_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() + 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)) - routing_network.create_route_carrier( + required = routing_network.create_route_carrier( doc, - [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="必经路径", project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) - routing_network.create_route_carrier( + forbidden = routing_network.create_route_carrier( doc, - [app.Vector(1000, 0, 20), app.Vector(1010, 0, 20)], + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="禁经路径", project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) - 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", - }, - ], - } + required.QetRouteConstraintMode = "Required" + forbidden.QetRouteConstraintMode = "Forbidden" + controller = auto_routing_panel.AutoRoutingController() - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"network_entry_max_distance": 30.0}, - ) + report = controller.clear_all_route_carrier_constraints() - 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([], wiring_objects.iter_routed_wire_objects(doc)) + self.assertEqual(2, report["route_constraint_carriers"]) + self.assertEqual("", required.QetRouteConstraintMode) + self.assertEqual("", forbidden.QetRouteConstraintMode) + self.assertNotIn("路径约束", controller.summary()) - def test_network_entry_uses_terminal_access_max_distance_when_smaller(self): + def test_selected_source_route_constraint_survives_carrier_regeneration(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(500, 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)] + first = routing_network.create_user_path_carriers_from_selection( doc, - [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], + selection, project_uuid="project-1", - kind="WireDuct", ) + routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") + routing_network.clear_route_carriers(doc) - route = auto_routing.build_network_route( - start, - end, - options={"terminal_access_max_distance": 30.0}, - doc=doc, + second = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", ) - self.assertIsNone(route) + self.assertEqual(1, len(first)) + self.assertEqual(1, len(second)) + self.assertEqual("Required", route_path.QetRouteConstraintMode) + self.assertEqual("Required", second[0].QetRouteConstraintMode) - def test_route_eplan_connections_writes_diagnostic_object_for_missing_terminal(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() + 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", - } - ], - } + 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" - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + refreshed = routing_network.create_user_path_carriers_from_selection( + doc, + selection, + project_uuid="project-1", + ) - 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.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) + self.assertEqual(1, len(refreshed)) + self.assertEqual("", refreshed[0].QetRouteConstraintMode) - def test_route_eplan_connections_writes_compact_batch_diagnostic(self): + 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() + 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", + 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)]), + ], ) - routing_network.create_route_carrier( + selection = [FakeSelectionItem(obj=route_path)] + carriers = routing_network.create_user_path_carriers_from_selection( doc, - [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], + selection, 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) + marked = routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - 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(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_compact_route_sample_prefers_route_track_bridged_segment_count(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() - sample = auto_routing._compact_route_sample( - { - "wire_uuid": "wire-bridge", - "route_track": { - "bridged_segments": 1, - }, - "network": { - "bridged_segments": 3, - }, - } + 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)]), + ], ) - - self.assertEqual(1, sample["network"]["bridged_segments"]) - - 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"}, - }, - ], - }, - } + 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: selection, ) - self.assertEqual({"WireDuct": 1}, sample["carrier_kinds"]) - self.assertEqual(["WireDuctA"], sample["carrier_names"]) + cleared = auto_routing_panel.AutoRoutingController().clear_selected_route_carrier_constraints() - def test_route_eplan_connections_batch_diagnostic_includes_quality_warnings(self): + self.assertEqual(2, cleared["route_constraint_carriers"]) + self.assertEqual("", route_path.QetRouteConstraintMode) + self.assertEqual(["", ""], [carrier.QetRouteConstraintMode for carrier in carriers]) + + 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") - _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( + 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(100, 0, 20)], + selection, 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", - } - ], - } + routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") - 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) + report = routing_network.clear_all_route_constraint_modes(doc) - 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"], - ) + 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_compact_batch_report_includes_entry_distance_warning_samples(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() - report = { - "routed": 1, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "terminal_access_warning_distance": 100.0, - "routes": [ - { - "wire_uuid": "wire-long-entry", - "wire_label": "N-LONG", - "network": { - "entry_distance": 125.0, - "exit_distance": 20.0, - }, - } - ], - } + 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), + ] + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) - payload = auto_routing._compact_routing_connection_batch_report(report) + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - 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(1, result["user_path_carriers"]) self.assertEqual( - ["entry"], - payload["route_entry_distance_warning_samples"][0]["warning_sides"], + [(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_route_eplan_connections_reports_total_connection_route_length(self): + def test_selected_user_path_copies_source_capacity(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") - _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", + 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)], ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "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) + auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carrier = routing_network.collect_route_carriers(doc)[0] - self.assertGreater(report["total_length_mm"], 0.0) - self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"]) - self.assertIn("总长度", message) + self.assertEqual(5, carrier.QetRouteCarrierCapacity) - def test_route_eplan_connections_hides_route_carriers_after_routing_by_default(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() + 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(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - carrier = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } + 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)]), ], - } - - report = auto_routing.route_eplan_connections( - doc, - payload=payload, - update_network=False, + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) - self.assertEqual(1, report["routed"]) - self.assertEqual(1, report["hidden_route_carriers"]) - self.assertFalse(carrier.ViewObject.Visibility) + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - def test_route_eplan_connections_batch_recomputes_once_after_created_wires(self): + self.assertEqual(2, result["user_path_carriers"]) + self.assertEqual([4, 4], [carrier.QetRouteCarrierCapacity for carrier in carriers]) + + 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") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 10, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 10, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", + face = FakeFace( + FakeBoundBox(0, 100, 0, 100, 0, 0), + app.Vector(0, 0, 1), ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], - project_uuid="project-1", - kind="WireDuct", + 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([face]), + FakeSelectionItem(obj=draft_line), + ], ) - recompute_count = {"value": 0} - def count_recompute(): - recompute_count["value"] += 1 + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) - doc.recompute = count_recompute - 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(1, result["user_path_carriers"]) + self.assertEqual([2.0, 2.0], [point.z for point in carriers[0].Points]) - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + 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") + 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: [], + ) - self.assertEqual(2, report["routed"]) - self.assertEqual(1, recompute_count["value"]) + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() - def test_route_eplan_connections_replaces_existing_routed_wires_for_same_batch(self): + 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_uses_terminal_local_route_points_before_main_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() 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)) + 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, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-repeat", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - }, - ], - } - first = auto_routing.route_eplan_connections_from_payload(doc, payload) - second = auto_routing.route_eplan_connections_from_payload(doc, payload) - routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + 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, first["routed"]) - self.assertEqual(1, second["routed"]) - self.assertEqual(1, second["replaced_routed_connections"]) - self.assertEqual(1, len(routed_wires)) - self.assertEqual("wire-repeat", routed_wires[0].QetWireUuid) + 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]], + ) - def test_clear_routing_connections_resets_task_status_and_batch_diagnostics(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() + 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)) + 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, 0, 20), app.Vector(100, 0, 20)], + [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], project_uuid="project-1", - kind="WireDuct", + kind="UserPath", ) - task = wiring_objects.create_wire_task( + + created = routing_network.create_terminal_access_carriers_from_document( doc, - "project-1", - "wire-clear", - "N1", - "terminal-start", - "terminal-end", - "instance-a", - "instance-b", + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=100.0, ) - report = auto_routing.route_eplan_connection_tasks(doc) - diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + 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, report["routed"]) - self.assertEqual("Routed", task.RouteStatus) - self.assertEqual(1, len(list(getattr(diagnostic_group, "Group", []) or []))) + 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 = _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)), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [ + FakeSelectionItem(obj=terminal), + FakeSelectionItem(obj=route_path), + ], + ) - removed = auto_routing.clear_routing_connections(doc) + 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, removed) - self.assertEqual("Task", task.RouteStatus) - self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) - self.assertEqual([], list(getattr(diagnostic_group, "Group", []) or [])) + 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_route_report_includes_route_source_sample_when_available(self): + 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() - report = { - "routed": 1, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ - { - "wire_label": "N4111", + 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))], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + 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, 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_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(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="UserPath", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual("Routed", result["route_status"]) + self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) + + 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") + 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() + second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + 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( + 2, + len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), + ) + + 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") + 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)], + ) + + 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 + ) + + 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_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") + 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 = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + + 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() + + self.assertEqual(3, len(generated)) + self.assertEqual([], routing_network.collect_route_carriers(doc)) + + 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() + 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)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=panel)], + ) + + 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" + ] + + 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() + 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, "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(0, 1, 20), app.Vector(5, 1, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + 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", + kind="WireDuct", + ) + + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + ) + + 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), + ) + + first_kind = getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "") + self.assertEqual("WireDuct", first_kind) + + 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") + _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="较远线槽", + ) + + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + terminal_exit_length=20.0, + max_distance=1000.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)) + + 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() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], + project_uuid="project-1", + kind="RoutingRange", + label="近处布线面", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 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="线槽接入桥", + ) + 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, + ) + + 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)) + + 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="近组件", + ) + 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, + } + ) + + 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() + 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", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 10, 20), app.Vector(120, 10, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + created = routing_network.create_terminal_access_carriers_from_document( + doc, + project_uuid="project-1", + ) + + 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_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(200, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + 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) + + 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() + 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_panel.AutoRoutingController().generate_routing_paths() + cut_out_carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" + ] + + self.assertEqual(1, result["wiring_cut_out_carriers"]) + self.assertEqual(1, len(cut_out_carriers)) + self.assertEqual("PassThrough", cut_out.QetRoutingObstacleMode) + + 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() + 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)) + + 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.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_wiring_cut_out_source_bridge_extension_controls_generated_path_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") + 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 + + 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.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_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") + 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", + ) + 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) + + self.assertEqual("Routed", result["route_status"]) + self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"]) + self.assertEqual(0, result["collision_count"]) + + 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() + 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) + + 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_check_routing_path_network_warns_for_long_terminal_access(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") + 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(1000, 0, 20), app.Vector(1100, 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, + ) + + result = auto_routing.check_eplan_routing_path_network( + 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) + + 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() + 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)], + 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="孤立布线面", + ) + + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + + self.assertNotIn("isolated_network_components", result["issue_codes"]) + self.assertEqual(0, len(result["diagnostic"]["isolated_components"])) + + 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() + 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)], + 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="UserPath", + label="孤立用户路径", + ) + + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + + self.assertIn("isolated_network_components", result["issue_codes"]) + self.assertEqual(2, len(result["diagnostic"]["isolated_components"])) + + 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() + 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", + label="孤立线槽", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], + project_uuid="project-1", + kind="TerminalAccess", + label="端子接入", + ) + + 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("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_zero_distance_user_path_endpoint_splits_wire_duct_segment(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + 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="端子接入", + ) + + result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") + + self.assertNotIn("wire_ducts_without_terminal_access", result["issue_codes"]) + + 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() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + duct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + label="线槽", + ) + main_path = routing_network.create_route_carrier( + doc, + [app.Vector(120, 20, 0), app.Vector(200, 20, 0)], + project_uuid="project-1", + kind="RoutingRange", + label="主网络", + ) + + created = routing_network.create_user_path_bridge_from_selection( + doc, + [ + types.SimpleNamespace(Object=duct), + types.SimpleNamespace(Object=main_path), + ], + project_uuid="project-1", + ) + + 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_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() + 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( + 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 + + created = routing_network.create_user_path_bridge_between_objects( + doc, + fallback_source, + current_source, + project_uuid="project-1", + ) + + 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) + + 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_local_route_points(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") + 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", + ) + 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.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.assertIn("端子局部路径无效", message) + self.assertIn("terminal-invalid-local-path", message) + + def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + 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]]) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], + project_uuid="project-1", + kind="WireDuct", + ) + 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"], + } + + message = auto_routing.format_routing_path_network_report(diagnostic) + + self.assertIn("布线路径网络检查发现", message) + self.assertIn("首个问题:external_issue", message) + + def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="线槽A", + project_uuid="project-1", + kind="WireDuct", + ) + + 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, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="坏用户路径", + project_uuid="project-1", + kind="UserPath", + ) + carrier.Points = [app.Vector(0, 0, 20)] + + 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, 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() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + 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": [], + } + + message = routing_network._routing_path_network_diagnostic_message(diagnostic) + + self.assertIn("布线路径网络检查通过", message) + + 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() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + 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( + doc, + project_uuid="project-1", + options={"adjoining_duct_tolerance": 15.0}, + ) + + 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() + + 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() + 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)) + + controller = auto_routing_panel.AutoRoutingController() + controller.set_terminal_access_max_distance(6000.0) + result = controller.generate_routing_paths() + + 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") + _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(900, 0, 20), app.Vector(1000, 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, + ) + 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")), + ) + 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") + + 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_route_eplan_connections_prepares_layout_space_like_eplan_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") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + duct = doc.addObject("Part::Feature", "WireDuctA") + duct.Label = "Wire Duct A" + duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) + app._qet_exchange_payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing_panel.AutoRoutingController().route_eplan_connections() + + 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_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") + _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", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(56, 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-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() + + self.assertEqual(1, report["routed"]) + self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) + + def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(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") + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + summary = auto_routing_panel.AutoRoutingController( + options={"adjoining_duct_tolerance": 15.0} + ).summary() + + self.assertIn("桥接:1", summary) + + def test_auto_routing_controller_summary_includes_runtime_version(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") + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), summary) + + def test_auto_routing_controller_summary_includes_cabinet_boundary_count(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") + boundary = doc.addObject("Part::Feature", "CabinetInteriorSpace") + boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) + boundary.QetRoutingBoundaryKind = "CabinetInterior" + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("柜内边界:1", summary) + + 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() + 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") + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("导线样式库:D:/project/project-local.sqlite", summary) + + 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() + 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") + + 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, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="UserPath", + ) + required.QetRouteConstraintMode = "Required" + forbidden = routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + project_uuid="project-1", + kind="UserPath", + ) + forbidden.QetRouteConstraintMode = "Forbidden" + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("路径约束:必经 1,禁经 1", summary) + + 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() + 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", "FutureUserRouteSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 100, 0, 80, 20, 20), + edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], + ) + route_path.QetRouteConstraintMode = "Required" + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("源路径约束:必经 1,禁经 0", summary) + + 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() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + 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" + + summary = auto_routing_panel.AutoRoutingController().summary() + + self.assertIn("源路径约束:必经 0,禁经 1", summary) + + def test_auto_routing_controller_exposes_lane_spacing(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, "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)], + 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", + }, + ], + } + + controller = auto_routing_panel.AutoRoutingController() + controller.set_lane_spacing(14.0) + report = controller.route_eplan_connections() + + 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_auto_routing_controller_exposes_lane_axis(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + 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_lane_spacing(8.0) + controller.set_lane_axis("z") + report = controller.route_eplan_connections() + + 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() + 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)], + 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( + doc, + start, + end, + route_index=21, + options=controller.routing_options(), + ) + + 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_auto_routing_controller_exposes_obstacle_clearance(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)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + 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(), + ) + + 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_auto_routing_controller_exposes_preflight_routeability_sample_limit(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_preflight_routeability_sample_limit(75) + + self.assertEqual(75, controller.routing_options()["preflight_routeability_sample_limit"]) + + 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") + _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( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + 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", + }, + ], + } + + 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) + + 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() + 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() + 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", + ) + + with self.assertRaises(auto_routing.AutoRoutingError): + auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + def test_route_eplan_connection_between_terminals_fails_without_network(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(120, 30, 0)) + + 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_surface_carrier_grid_uses_actual_rotated_face_plane(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") + 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( + doc, + [FakeSelectionItem([face])], + 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()], + ) + + created = routing_network.create_carriers_from_selection( + doc, + [FakeSelectionItem(obj=solid)], + project_uuid="project-1", + ) + + self.assertEqual([], created) + + def test_route_path_creation_splits_disconnected_shape_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + 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, + [FakeSelectionItem(obj=route_path)], + project_uuid="project-1", + ) + + 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_route_path_creation_projects_line_to_selected_face(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") + 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), + ], + 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, + ) + + 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_wire_duct_source_end_margin_controls_generated_centerline_length(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)) + 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 + + created = routing_network.create_wire_duct_carriers_from_selection( + doc, + [FakeSelectionItem(obj=duct)], + project_uuid="project-1", + margin=20.0, + ) + + 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(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, + [FakeSelectionItem(obj=duct)], + project_uuid="project-1", + margin=0.0, + ) + + 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"]) + + 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"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + 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_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(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}, + ) + + 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"]) + + 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(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)) + + result = auto_routing.route_eplan_connection_between_terminals( + 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("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_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, 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}, + ) + + 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"] + 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, 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, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + 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_eplan_connection_wire_records_boundary_warning_when_route_leaves_cabinet(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, 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" + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + 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, + ) + + def test_eplan_connection_wire_records_long_network_access_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(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", + ) + + 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, + }, + ) + + 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() + 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, 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", + ) + 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}, + ) + + 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_eplan_connection_route_tolerates_missing_route_constraint_collector(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", + ) + 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 + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual({}, result["network"].get("route_constraints", {})) + + 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") + 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", + ) + + 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": ["禁止路径"], + }, + ) + + labels = [ + segment["carrier"]["label"] + for segment in result["route_track"]["segments"] + ] + self.assertIn("允许路径", labels) + self.assertNotIn("禁止路径", labels) + + 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") + 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="远路径", + project_uuid="project-1", + kind="UserPath", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + 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"], + ) + + 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") + 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 = "禁止经过" + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="远路径", + project_uuid="project-1", + kind="UserPath", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + 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"], + ) + + 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="远路径", + project_uuid="project-1", + kind="UserPath", + ) + required.QetRouteConstraintMode = "Required" + + result = auto_routing.route_eplan_connection_between_terminals( + 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( + required.Name, + result["network"]["route_constraints"]["required"]["names"], + ) + + 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") + 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", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, + ) + + 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 test_eplan_connection_route_requires_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") + 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", + ) + + 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": ["必经路径"], + }, + ) + + 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_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(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", + ) + + 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)) + + 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(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}, + ) + + 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_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(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) + + 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 = "第三方设备" + terminal_objects.ensure_string_property( + obstacle, + "QetElementUuid", + "QET Exchange", + "", + "device-obstacle", + ) + 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, + "avoid_local_access_obstacles": True, + "terminal_exit_length": 0.0, + }, + endpoint_metadata={ + "start_element_uuid": "device-start", + "end_element_uuid": "device-end", + }, + ) + + 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_network_route_limits_local_access_obstacles_to_nearby_bboxes(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", + ) + 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) + + 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(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_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 + + 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) + + 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"] + 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", + ) + 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) + + 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 + + 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") + 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", + ) + 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}, + ) + + 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_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") + 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", + ) + terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") + terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + + 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") + 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(20, 80, 0), app.Vector(120, 80, 0)], + label="Cabinet Main Path", + project_uuid="project-1", + ) + local_body = doc.addObject("Part::Feature", "StartDeviceLocalShell") + local_body.Shape = FakeShape(FakeBoundBox(15, 25, 15, 25, -5, 5)) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + ) + + 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_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") + 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(20, 80, 0), app.Vector(120, 80, 0)], + label="Cabinet Main Path", + project_uuid="project-1", + ) + main_obstacle = doc.addObject("Part::Feature", "MainPathObstacle") + main_obstacle.Shape = FakeShape(FakeBoundBox(55, 65, 75, 85, -5, 5)) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, + ) + + self.assertEqual("CollisionWarning", result["route_status"]) + self.assertEqual(1, result["collision_count"]) + self.assertEqual("MainPathObstacle", result["collisions"][0]["obstacle_name"]) + + 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") + 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", + ) + 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) + + 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"]), + "局部接入段应增加侧向绕障拐点,而不是直接穿过障碍盒。", + ) + + 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() + 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)) + 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", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + + def test_route_eplan_connections_from_payload_skips_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 = { + "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", + } + ] + } + + 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_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_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_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, + 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 + + 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_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, + options={"auto_create_main_path_detour_bridges": True}, + 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_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", "third_party_device_collisions"], + payload["route_samples"][0]["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_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", + "route_quality_warnings", + "route_capacity_pressure", + "route_candidate_boundary_violations", + ], + sample["issue_codes"], + ) + 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", + } + }, + ] + }, + } + ) + + self.assertEqual( + ["多路径草图(路径1)", "多路径草图(路径2)"], + sample["route_source_labels"], + ) + + 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({"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", + } + ], + } + + 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(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"], + ) + + def test_compact_batch_report_includes_entry_distance_warning_samples(self): + _install_fake_freecad() + _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_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"}}, + ], + }, + } + ], + } + + 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"], + ) + 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_uuid": "wire-surface", + "wire_label": "N-SURFACE", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "RoutingRange", + "label": "安装板辅助路径", + } + } + ], + }, + } + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual( + ["安装板辅助路径"], + payload["route_quality_warning_samples"][0]["route_carrier_labels"], + ) + + 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 = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "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) + + 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.assertEqual( + ["绕行路径A"], + payload["route_candidate_obstacle_warning_samples"][0]["route_source_labels"], + ) + + def test_compact_batch_report_includes_candidate_boundary_warning_samples(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-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( + "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_batch_report_includes_route_constraint_samples(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": ["禁止路径"]}, + }, + }, + } + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + 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"], + ) + + def test_compact_batch_report_includes_capacity_pressure_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}}, + ] + }, + } + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + 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"], + ) + + 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() + 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( + ["黄色主路径(路径1)"], + payload["route_capacity_pressure_warning_samples"][0]["route_source_labels"], + ) + + def test_compact_batch_report_includes_collision_kind_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, + "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": [ + { + "collision_kind": "HardIntersection", + "collision_relation": "third_party_device_collision", + "obstacle_label": "设备A", + }, + ], + }, + { + "collision_samples": [ + { + "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( + "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_batch_report_includes_top_collision_obstacles(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 3, + "routed": 3, + "collision_warnings": 3, + "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"], + }, + ], + }, + { + "collision_samples": [ + { + "collision_kind": "HardIntersection", + "obstacle_name": "BracketBObject", + "obstacle_label": "支架B", + "obstacle_parent_labels": ["柜体总成"], + "obstacle_parent_names": ["CabinetAssembly"], + }, + ], + }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual( + [ + { + "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_summarizes_collision_resolution_categories(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 2, + "skipped_missing_terminal": 0, + "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", + }, + ], + }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual( + { + "review_device_or_layout_collision": 1, + "review_pass_through_structural_obstacle": 2, + }, + payload["collision_resolution_summary"]["counts"], + ) + self.assertEqual( + "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;另有 1 个疑似设备/装配碰撞需要补路径或调整装配。", + payload["collision_resolution_summary"]["recommended_action"], + ) + + 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": 2, + "collision_warnings": 2, + "skipped_missing_terminal": 0, + "routes": [ + { + "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.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_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": 2, + "missing_endpoint_uuids": ["terminal-missing-a", "terminal-missing-b"], + "missing_endpoint_samples": [ + { + "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.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"], + ) + 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): + _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}, + } + }, + "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_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": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "label": "线槽A"}}, + ], + }, + }, + { + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, + ], + }, + }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertEqual( + {"main_path_routes": 1, "fallback_routes": 1}, + payload["route_path_usage"], + ) + + 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": 2, + "routed": 2, + "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": "安装板辅助路径"}}, + ], + }, + }, + { + "route_track": { + "segments": [ + {"carrier": {"kind": "AuxiliaryPath", "label": "门板辅助路径"}}, + ], + }, + }, + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + 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), + ) + + def test_route_eplan_connections_report_includes_top_level_path_usage_summary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual( + {"main_path_routes": 1, "fallback_routes": 0}, + report["route_path_usage"], + ) + self.assertEqual([], report["top_collision_obstacles"]) + + def test_route_eplan_connections_attaches_path_diagnostic_when_main_path_exists_but_unused(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", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(500, 0, 20), app.Vector(600, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + label="孤立线槽", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + original_diagnostic = routing_network.diagnose_routing_path_network + + def fake_diagnostic(*_args, **_kwargs): + return { + "ok": False, + "issues": [ + { + "severity": "warning", + "code": "wire_ducts_without_terminal_access", + "count": 1, + }, + ], + "summary": {"carriers": 2}, + "wire_ducts_without_terminal_access": [ + { + "index": 0, + "nodes": 2, + "segments": 1, + "carrier_kinds": {"WireDuct": 1}, + "carrier_names": ["孤立线槽"], + "bridge_suggestion": {"distance_mm": 42.0}, + }, + ], + } + + routing_network.diagnose_routing_path_network = fake_diagnostic + try: + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + finally: + routing_network.diagnose_routing_path_network = original_diagnostic + + self.assertIn("main_path_not_used", report["issue_codes"]) + diagnostic = report["routing_path_network_diagnostic"] + self.assertIn("wire_ducts_without_terminal_access", diagnostic["issue_codes"]) + self.assertEqual( + "孤立线槽", + diagnostic["wire_ducts_without_terminal_access"][0]["carrier_names"][0], + ) + + def test_route_eplan_connections_reports_total_connection_route_length(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertGreater(report["total_length_mm"], 0.0) + self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"]) + self.assertIn("总长度", message) + + def test_route_eplan_connections_hides_route_carriers_after_routing_by_default(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)) + carrier = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + update_network=False, + ) + + self.assertEqual(1, report["routed"]) + self.assertEqual(1, report["hidden_route_carriers"]) + self.assertFalse(carrier.ViewObject.Visibility) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + diagnostics = [ + item + for item in list(getattr(diagnostic_group, "Group", []) or []) + if getattr(item, "QetDiagnosticKind", "") == "RoutingConnectionBatch" + ] + diagnostic_payload = json.loads(diagnostics[0].QetDiagnosticJson) + self.assertEqual(1, diagnostic_payload["hidden_route_carriers"]) + self.assertTrue(diagnostic_payload["routing_path_network_updated"] is False) + + def test_route_eplan_connections_ignores_global_payload_from_other_project(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-current") + _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-current", + kind="WireDuct", + ) + wiring_objects.create_wire_task( + doc, + "project-current", + "wire-current", + "CURRENT", + "terminal-start", + "terminal-end", + "", + "", + ) + app._qet_exchange_payload = { + "project_uuid": "project-old", + "wires": [ + { + "wire_id": "wire-old", + "wire_label": "OLD", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections(doc, update_network=False) + + self.assertEqual("project-current", report["project_uuid"]) + self.assertEqual(1, report["routed"]) + self.assertEqual("wire-current", report["routes"][0]["wire_uuid"]) + + def test_route_eplan_connections_batch_recomputes_once_after_created_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 10, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 10, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + recompute_count = {"value": 0} + + def count_recompute(): + recompute_count["value"] += 1 + + doc.recompute = count_recompute + 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) + + self.assertEqual(2, report["routed"]) + self.assertEqual(1, recompute_count["value"]) + + def test_route_eplan_connections_replaces_existing_routed_wires_for_same_batch(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-repeat", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + + first = auto_routing.route_eplan_connections_from_payload(doc, payload) + second = auto_routing.route_eplan_connections_from_payload(doc, payload) + routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) + + self.assertEqual(1, first["routed"]) + self.assertEqual(1, second["routed"]) + self.assertEqual(1, second["replaced_routed_connections"]) + self.assertEqual(1, len(routed_wires)) + self.assertEqual("wire-repeat", routed_wires[0].QetWireUuid) + + def test_clear_routing_connections_resets_task_status_and_batch_diagnostics(self): + _install_fake_freecad() + terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _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", + ) + task = wiring_objects.create_wire_task( + doc, + "project-1", + "wire-clear", + "N1", + "terminal-start", + "terminal-end", + "instance-a", + "instance-b", + ) + + report = auto_routing.route_eplan_connection_tasks(doc) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + + self.assertEqual(1, report["routed"]) + self.assertEqual("Routed", task.RouteStatus) + self.assertEqual(1, len(list(getattr(diagnostic_group, "Group", []) or []))) + + removed = auto_routing.clear_routing_connections(doc) + + self.assertEqual(1, removed) + self.assertEqual("Task", task.RouteStatus) + self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) + self.assertEqual([], list(getattr(diagnostic_group, "Group", []) or [])) + + def test_route_report_includes_route_source_sample_when_available(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_label": "N4111", + "route_track": { + "segments": [ + {"carrier": {"kind": "TerminalAccess", "source_label": "QF1:A1"}}, + {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + {"carrier": {"kind": "WiringCutOut", "source_label": "过线孔A"}}, + {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message) + + def test_route_report_source_sample_falls_back_to_carrier_label(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_label": "N4111", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "WireDuct", + "label": "手动线槽", + "name": "ManualDuct", + } + }, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 手动线槽。", message) + + def test_route_report_source_sample_skips_bridge_segments(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_label": "N4111", + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + {"is_bridge": True, "carrier": {"kind": "WireDuct", "source_label": "虚拟桥接"}}, + {"carrier": {"kind": "UserPath", "source_label": "用户路径B"}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 线槽A、用户路径B。", message) + self.assertNotIn("虚拟桥接", message) + + def test_route_report_source_sample_includes_user_path_source_index(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_label": "N4111", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "UserPath", + "source_label": "多路径草图", + "source_path_index": "2", + } + }, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 多路径草图(路径2)。", message) + + def test_route_report_source_sample_includes_user_path_source_index_one_when_present(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_label": "N4111", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "UserPath", + "source_label": "用户路径A", + "source_path_index": "1", + } + }, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径示例:导线 N4111 经过 用户路径A(路径1)。", message) + + def test_route_track_segment_keys_skip_bridge_segments(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + route_track = { + "segments": [ + { + "from_key": [0, 0, 0], + "to_key": [100, 0, 0], + "carrier": {"name": "WireDuctA"}, + }, + { + "is_bridge": True, + "from_key": [100, 0, 0], + "to_key": [100, 10, 0], + "carrier": {"name": "VirtualBridge"}, + }, + ] + } + + keys = auto_routing._route_track_segment_keys(route_track) + + self.assertEqual(1, len(keys)) + self.assertEqual("WireDuctA", keys[0][0]) + + def test_route_quality_warning_ignores_bridge_only_routing_range(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_label": "N4111", + "route_track": { + "carrier_kinds": {"RoutingRange": 1}, + "segments": [ + { + "is_bridge": True, + "carrier": {"kind": "RoutingRange", "source_label": "虚拟布线面桥接"}, + } + ], + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertNotIn("路径质量提示", message) + + def test_route_quality_warning_includes_specific_carrier_label(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_label": "N4111", + "route_track": { + "segments": [ + { + "carrier": { + "kind": "RoutingRange", + "label": "安装板辅助路径", + } + } + ], + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("示例 N4111 使用布线面:安装板辅助路径。", message) + + def test_route_report_includes_main_path_and_fallback_usage_counts(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_label": "N-WD", + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "label": "线槽A"}}, + ], + }, + }, + { + "wire_label": "N-RANGE", + "route_track": { + "segments": [ + {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, + ], + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径采用:线槽/主路径 1 条,布线面/辅助路径 1 条。", message) + + def test_route_report_includes_network_bridge_and_blocked_segment_counts(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": [ + { + "network": { + "bridged_segments": 1, + "blocked_segments": 2, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径,避障屏蔽 2 段。", message) + + def test_route_report_prefers_route_track_bridged_segment_count(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": [ + { + "network": { + "bridged_segments": 3, + }, + "route_track": { + "bridged_segments": 1, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径。", message) + self.assertNotIn("自动桥接 3 段", message) + + def test_route_report_includes_parallel_lane_summary(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + {"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": 0.0}}, + {"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": -10.0}}, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("并行错位:最大 lane 2,间距 10.0 mm,最大偏移 30.0 mm。", message) + + def test_eplan_connection_lane_offset_is_capped_for_dense_parallel_routes(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + route_index=21, + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + + self.assertEqual(30.0, result["lane"]["offset_mm"]) + self.assertLessEqual( + max(abs(point.y) for point in result["points"]), + 30.0, + ) + + def test_route_report_includes_replaced_routed_connection_count(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "replaced_routed_connections": 2, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("已替换旧布线连接:2 条。", message) + + def test_route_report_includes_hidden_route_carrier_count(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "hidden_route_carriers": 3, + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("已隐藏走线路径辅助对象:3 条。", message) + + def test_route_report_warns_when_routes_use_surface_or_auxiliary_paths(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "wire_label": "N1", + "route_track": { + "carrier_kinds": { + "TerminalAccess": 2, + "WireDuct": 1, + "RoutingRange": 2, + }, + }, + }, + { + "wire_label": "N2", + "route_track": { + "carrier_kinds": { + "TerminalAccess": 2, + "WireDuct": 3, + }, + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径质量提示:1 条导线使用布线面/辅助路径", message) + self.assertIn("示例 N1 使用布线面。", message) + + def test_route_report_warns_when_parallel_lanes_exceed_track_capacity(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": [ + { + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "capacity": 2}}, + {"carrier": {"kind": "WireDuct", "capacity": 4}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("容量提示:最大并行线数 3,路径最小容量 2。", message) + + def test_route_report_capacity_pressure_includes_sample_wire(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}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("示例导线 N-CROWDED", message) + 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() + 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": "UserPath", + "name": "QETRoutePath_001", + "capacity": 1, + "source_label": "黄色主路径", + "source_path_index": "1", + } + } + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径 黄色主路径(路径1)", message) + + def test_route_report_ignores_bridge_segments_for_capacity_pressure(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": [ + { + "lane": {"index": 2, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"is_bridge": True, "carrier": {"kind": "UserPath", "capacity": 1}}, + {"carrier": {"kind": "WireDuct", "capacity": 4}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertNotIn("容量提示", message) + + def test_route_report_includes_entry_candidate_rank_when_route_uses_fallback_entry(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_label": "N1", + "network": { + "entry_candidate_rank": 3, + "exit_candidate_rank": 1, + "entry_candidate_score": 125.0, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("接入候选", message) + self.assertIn("起点第 3 个", message) + + def test_route_report_includes_missing_route_retry_summary(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 75, + "routed": 71, + "collision_warnings": 0, + "skipped_missing_terminal": 4, + "missing_route_retries": 12, + "missing_route_retry_candidate_limit": 8, + "routes": [], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("候选放宽重试:12 条导线", message) + self.assertIn("候选上限 8", message) + + def test_route_report_warns_when_network_entry_distance_is_long(self): + _install_fake_freecad() + _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_label": "N1", + "network": { + "entry_distance": 125.0, + "exit_distance": 20.0, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "label": "主线槽A"}}, + ], + }, + }, + { + "wire_label": "N2", + "network": { + "entry_distance": 20.0, + "exit_distance": 150.0, + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("接入距离提示:2 条导线", message) + self.assertIn("示例导线 N1", message) + self.assertIn("起点接入 125.0 mm", message) + self.assertIn("路径 主线槽A", message) + + def test_route_report_warns_when_candidate_route_still_hits_obstacles(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_label": "N1", + "network": { + "route_candidate_obstacle_hits": 2, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "UserPath", "label": "绕行路径A"}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("接入避障提示:1 条导线候选路径仍穿过障碍", message) + self.assertIn("示例导线 N1 2 处", message) + self.assertIn("路径 绕行路径A", message) + + def test_route_report_warns_when_candidate_route_still_leaves_cabinet_boundary(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_label": "N1", + "network": { + "boundary_aware": True, + "route_candidate_boundary_violations": 3, + }, + "route_track": { + "segments": [ + {"carrier": {"kind": "UserPath", "label": "柜内主路径A"}}, + ] + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("柜内边界提示:1 条导线最终路径仍越出柜内区域", message) + self.assertIn("示例导线 N1 3 个越界点", message) + self.assertIn("路径 柜内主路径A", message) + + def test_route_report_includes_route_constraint_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": [ + { + "wire_label": "N1", + "network": { + "route_constraints": { + "required": {"labels": ["必经路径"]}, + "forbidden": {"labels": ["禁止路径"]}, + }, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("路径约束提示:1 条导线应用必经/禁经规则", message) + self.assertIn("示例导线 N1", message) + self.assertIn("必须经过 必经路径", message) + self.assertIn("禁止经过 禁止路径", message) + + def test_route_report_prefers_constraint_source_label_over_internal_source_name(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_label": "N1", + "network": { + "route_constraints": { + "required": { + "source_names": ["YellowMainRouteSketch"], + "source_labels": ["黄色主路径"], + }, + }, + }, + } + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn("必须经过 源标签 黄色主路径", message) + self.assertNotIn("YellowMainRouteSketch", message) + + def test_compact_batch_report_does_not_treat_route_constraints_as_issue(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, + "routes": [ + { + "wire_uuid": "wire-required", + "network": { + "route_constraints": { + "required": { + "source_labels": ["黄色主路径"], + }, + }, + }, + } + ], + } + + payload = auto_routing._compact_routing_connection_batch_report(report) + + self.assertNotIn("route_constraints", payload["issue_codes"]) + self.assertEqual(1, payload["route_constraint_warning_count"]) + + def test_route_report_capacity_pressure_is_checked_per_route(self): + _install_fake_freecad() + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "routes": [ + { + "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ - {"carrier": {"kind": "TerminalAccess", "source_label": "QF1:A1"}}, - {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, - {"carrier": {"kind": "WiringCutOut", "source_label": "过线孔A"}}, - {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, + {"carrier": {"kind": "WireDuct", "capacity": 4}}, ] }, + }, + { + "lane": {"index": 0, "spacing_mm": 10.0}, + "route_track": { + "segments": [ + {"carrier": {"kind": "WireDuct", "capacity": 1}}, + ] + }, + }, + ], + } + + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertNotIn("容量提示", message) + + def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "wire_style_id": "42", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 12.0, "lane_axis": "y"}, + ) + route = report["routes"][0] + + self.assertEqual("wire-1", route["wire_uuid"]) + self.assertEqual("N4111", route["wire_label"]) + self.assertEqual("42", route["wire_style_id"]) + self.assertEqual("terminal-start", route["start_terminal_uuid"]) + self.assertEqual("terminal-end", route["end_terminal_uuid"]) + self.assertEqual(0, route["lane"]["index"]) + self.assertEqual("network-dijkstra-v1", route["algorithm"]) + self.assertEqual(1, route["network"]["carriers"]) + self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + + def test_route_eplan_connections_can_skip_nearer_isolated_entry_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, 1, 20), app.Vector(5, 1, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(1, report["routed"]) + self.assertEqual(0, len(report["errors"])) + route = report["routes"][0] + self.assertEqual("network-dijkstra-v1", route["algorithm"]) + self.assertGreater(route["network"]["entry_distance"], 1.0) + self.assertGreater(route["network"]["entry_candidate_rank"], 1) + + def test_route_eplan_connections_report_includes_routing_path_network_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, "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-range-only", + "wire_label": "N-RANGE", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + options={"hide_route_carriers_after_route": False}, + project_uuid="project-1", + ) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual(1, report["routed"]) + self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) + self.assertIn( + "routing_range_only_network", + report["routing_path_network_diagnostic"]["issue_codes"], + ) + self.assertIn("路径网络检查提示", message) + self.assertIn("仅使用布线面兜底", message) + + def test_route_eplan_connections_path_network_diagnostic_uses_terminal_access_warning_distance(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)) + routing_network.create_route_carrier( + doc, + [app.Vector(900, 0, 20), app.Vector(1000, 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, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-long-access", + "wire_label": "N-LONG", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + options={ + "hide_route_carriers_after_route": False, + "terminal_access_max_distance": 1000.0, + "terminal_access_warning_distance": 950.0, + }, + project_uuid="project-1", + update_network=False, + ) + + self.assertNotIn( + "long_terminal_accesses", + report["routing_path_network_diagnostic"]["issue_codes"], + ) + + def test_route_eplan_connections_path_network_diagnostic_keeps_long_access_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") + device = doc.addObject("App::Part", "DevicePEN") + device.Label = "PEN" + device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + device.addObject(start) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(1000, 0, 20), app.Vector(1100, 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, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-long-access", + "wire_label": "N-LONG", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + options={ + "hide_route_carriers_after_route": False, + "terminal_access_max_distance": 1000.0, + }, + project_uuid="project-1", + update_network=False, + ) + + diagnostic = report["routing_path_network_diagnostic"] + self.assertIn("long_terminal_accesses", diagnostic["issue_codes"]) + self.assertEqual(1, len(diagnostic["long_terminal_accesses"])) + sample = diagnostic["long_terminal_accesses"][0] + self.assertEqual("terminal-start", sample["terminal_uuid"]) + self.assertEqual("PEN", sample["parent_device_label"]) + self.assertEqual("x", sample["terminal_access_dominant_axis"]) + self.assertEqual(2, len(sample["terminal_access_points"])) + + def test_route_report_includes_outside_boundary_path_network_sample(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)) + boundary = doc.addObject("Part::Feature", "CabinetBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(0, 120, -20, 20, 0, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20), app.Vector(140, 0, 20)], + label="柜内主路径A", + project_uuid="project-1", + kind="UserPath", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-outside-source", + "wire_label": "N-OUT", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + options={"hide_route_carriers_after_route": False}, + project_uuid="project-1", + update_network=False, + ) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn( + "route_carriers_outside_boundary", + report["routing_path_network_diagnostic"]["issue_codes"], + ) + self.assertEqual( + "柜内主路径A", + report["routing_path_network_diagnostic"]["route_carriers_outside_boundary"][0]["carrier"]["label"], + ) + self.assertIn("路径网络检查提示:路径越出柜内边界", message) + self.assertIn("越界路径:柜内主路径A 1 个越界点", message) + + def test_route_report_includes_outside_boundary_terminal_sample(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, "TerminalInside", "terminal-inside", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) + boundary = doc.addObject("Part::Feature", "CabinetBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(-20, 120, -20, 20, -10, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="柜内主路径A", + project_uuid="project-1", + kind="UserPath", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-outside-terminal", + "wire_label": "N-OUT-TERM", + "start_terminal_uuid": "terminal-inside", + "end_terminal_uuid": "terminal-outside", + } + ], + } + + report = auto_routing.route_eplan_connections( + doc, + payload=payload, + options={"hide_route_carriers_after_route": False}, + project_uuid="project-1", + update_network=False, + ) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertIn( + "terminals_outside_boundary", + report["routing_path_network_diagnostic"]["issue_codes"], + ) + self.assertEqual( + "terminal-outside", + report["routing_path_network_diagnostic"]["terminals_outside_boundary"][0]["terminal_uuid"], + ) + self.assertIn("路径网络检查提示:端子未接入、端子越出柜内边界", message) + self.assertIn("越界端子:TerminalOutside(terminal-outside) 2 个越界点", message) + + def test_route_eplan_connections_preserves_endpoint_metadata_on_routed_wire(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "start_element_uuid": "device-a", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "A1", + "end_element_uuid": "device-b", + "end_terminal_uuid": "terminal-end", + "end_terminal_display": "B1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = routed_group.Group[0] + diagnostics = json.loads(wire.QetRouteDiagnosticsJson) + + self.assertEqual("device-a", wire.QetStartElementUuid) + self.assertEqual("A1", wire.QetStartTerminalDisplay) + self.assertEqual("device-b", wire.QetEndElementUuid) + self.assertEqual("B1", wire.QetEndTerminalDisplay) + self.assertEqual("device-a", report["routes"][0]["start_element_uuid"]) + self.assertEqual("B1", report["routes"][0]["end_terminal_display"]) + self.assertEqual("A1", diagnostics["endpoint_metadata"]["start_terminal_display"]) + + def test_route_eplan_connection_tasks_preserve_task_endpoint_labels_on_routed_wire(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", + ) + task = wiring_objects.create_wire_task( + doc, + "project-1", + "wire-1", + "N4111", + "terminal-start", + "terminal-end", + "instance-a", + "instance-b", + ) + terminal_objects.ensure_string_property(task, "QetStartDeviceLabel", "QET Wiring", "", "QF1") + terminal_objects.ensure_string_property(task, "QetEndDeviceLabel", "QET Wiring", "", "X1") + terminal_objects.ensure_string_property(task, "QetEndpointLabel", "QET Wiring", "", "QF1:A1 -> X1:B1") + + report = auto_routing.route_eplan_connection_tasks(doc) + routed_group = doc.getObject("QETWiring_04_Routed") + wire = routed_group.Group[0] + diagnostics = json.loads(wire.QetRouteDiagnosticsJson) + + self.assertEqual("QF1", wire.QetStartDeviceLabel) + self.assertEqual("X1", wire.QetEndDeviceLabel) + self.assertEqual("QF1:A1 -> X1:B1", wire.QetEndpointLabel) + self.assertEqual("QF1:A1 -> X1:B1", report["routes"][0]["endpoint_label"]) + self.assertEqual("QF1", diagnostics["endpoint_metadata"]["start_device_label"]) + + def test_route_eplan_connections_records_wire_identity_for_errors(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, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-bad", + "wire_label": "N500", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-start", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + self.assertEqual(1, len(report["errors"])) + self.assertIn("error_samples", report) + self.assertEqual("wire-bad", report["error_samples"][0]["wire_uuid"]) + self.assertEqual("N500", report["error_samples"][0]["wire_label"]) + self.assertEqual("N500", report["error_samples"][0]["wire_object_label"]) + self.assertEqual("terminal-start", report["error_samples"][0]["start_terminal_uuid"]) + self.assertEqual("terminal-start", report["error_samples"][0]["end_terminal_uuid"]) + self.assertIn("different", report["error_samples"][0]["error"]) + + def test_route_eplan_connections_report_includes_readable_error_sample(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, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-bad", + "wire_label": "N500", + "start_element_uuid": "device-a", + "start_terminal_uuid": "terminal-start", + "start_terminal_display": "A1", + "end_element_uuid": "device-a", + "end_terminal_uuid": "terminal-start", + "end_terminal_display": "A1", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual("device-a", report["error_samples"][0]["start_element_uuid"]) + self.assertIn("错误示例:导线 N500", message) + self.assertIn("device-a/A1 (terminal-start) -> device-a/A1 (terminal-start)", message) + + def test_route_eplan_connections_counts_route_statuses_for_summary(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "RouteStart", "route-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "RouteEnd", "route-end", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "CollisionStart", "collision-start", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "CollisionEnd", "collision-end", app.Vector(100, 100, 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, 100, 100), app.Vector(100, 100, 100)], + project_uuid="project-1", + kind="WireDuct", + ) + obstacle = doc.addObject("Part::Feature", "CollisionObstacle") + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 90, 110, 90, 110)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-ok", + "start_terminal_uuid": "route-start", + "end_terminal_uuid": "route-end", + }, + { + "wire_id": "wire-collision", + "start_terminal_uuid": "collision-start", + "end_terminal_uuid": "collision-end", + }, + { + "wire_id": "wire-error", + "start_terminal_uuid": "route-start", + "end_terminal_uuid": "route-start", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"avoid_obstacles": False, "avoid_local_access_obstacles": False}, + ) + message = auto_routing.format_eplan_connection_route_report(report) + + self.assertEqual(1, report["route_status_counts"]["Routed"]) + self.assertEqual(1, report["route_status_counts"]["CollisionWarning"]) + self.assertEqual(1, report["route_status_counts"]["Error"]) + self.assertIn("结果状态", message) + self.assertIn("正常 1 条", message) + self.assertIn("碰撞告警 1 条", message) + self.assertIn("错误 1 条", message) + + def test_route_eplan_connections_lane_index_is_per_terminal_pair(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _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, 100, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 100, 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, 100, 20), app.Vector(100, 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", + }, + { + "wire_id": "wire-a-repeat", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + + self.assertEqual(0, report["routes"][0]["lane"]["index"]) + self.assertEqual(0, report["routes"][1]["lane"]["index"]) + self.assertEqual(1, report["routes"][2]["lane"]["index"]) + + def test_route_eplan_connections_lane_index_increments_for_shared_route_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, "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)], + 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, "lane_axis": "y"}, + ) + + self.assertEqual(0, report["routes"][0]["lane"]["index"]) + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + routed_group = doc.getObject("QETWiring_04_Routed") + self.assertEqual(2, len(list(getattr(routed_group, "Group", []) or []))) + second_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "wire-b" + ][0] + self.assertEqual("1", second_wire.QetRouteLaneIndex) + self.assertEqual("y", second_wire.QetRouteLaneAxis) + self.assertEqual("10.000", second_wire.QetRouteLaneOffsetMm) + self.assertEqual("CapacityWarning", second_wire.QetRouteCapacityStatus) + self.assertEqual("2", second_wire.QetRouteParallelWireCount) + 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_lane_index_accounts_for_existing_routed_segments(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="existing-wire", + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "new-wire", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0, "lane_axis": "y"}, + ) + + self.assertEqual(1, report["routes"][0]["lane"]["index"]) + routed_group = doc.getObject("QETWiring_04_Routed") + new_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "new-wire" + ][0] + self.assertEqual("1", new_wire.QetRouteLaneIndex) + self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in new_wire.Points[1:-1])) + + def test_route_eplan_connections_auto_lane_axis_offsets_perpendicular_to_shared_segment(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"lane_spacing": 10.0}, + ) + + self.assertEqual(1, report["routes"][1]["lane"]["index"]) + self.assertEqual("x", report["routes"][1]["lane"]["axis"]) + routed_group = doc.getObject("QETWiring_04_Routed") + second_wire = [ + wire + for wire in list(getattr(routed_group, "Group", []) or []) + if getattr(wire, "QetWireUuid", "") == "wire-b" + ][0] + self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + + def test_route_eplan_connections_prefers_unused_alternate_route_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, "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( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + first_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + second_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][1]["route_track"]["segments"] + ] + self.assertIn("Direct Duct", first_labels) + self.assertIn("Alternate Duct", second_labels) + self.assertNotIn("Direct Duct", second_labels) + + def test_route_eplan_connections_respects_route_segment_capacity_before_detouring(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartC", "terminal-start-c", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndC", "terminal-end-c", app.Vector(100, 0, 0)) + direct = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="Direct Duct", + project_uuid="project-1", + kind="WireDuct", + ) + direct.QetRouteCarrierCapacity = 2 + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], + label="Left Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Alternate Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + { + "wire_id": "wire-c", + "start_terminal_uuid": "terminal-start-c", + "end_terminal_uuid": "terminal-end-c", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + route_labels = [ + [segment["carrier"]["label"] for segment in route["route_track"]["segments"]] + for route in report["routes"] + ] + self.assertIn("Direct Duct", route_labels[0]) + self.assertIn("Direct Duct", route_labels[1]) + self.assertIn("Alternate Duct", route_labels[2]) + self.assertNotIn("Direct Duct", route_labels[2]) + + def test_route_eplan_connections_prefers_unused_segments_occupied_by_existing_wires(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + 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( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + auto_routing.route_eplan_connection_between_terminals( + doc, + start, + end, + wire_uuid="existing-wire", + ) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "new-wire", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + route_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + self.assertIn("Alternate Duct", route_labels) + self.assertNotIn("Direct Duct", route_labels) + + def test_route_eplan_connections_report_includes_collision_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)) + _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", "MiddleObstacle") + obstacle.Label = "Middle Obstacle" + terminal_objects.ensure_string_property( + obstacle, + "QetElementUuid", + "QET Exchange", + "", + "device-obstacle", + ) + terminal_objects.ensure_string_property( + obstacle, + "QetInstanceId", + "QET Exchange", + "", + "instance-obstacle", + ) + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "wire_style_id": "style-1", + "start_element_uuid": "device-start", + "start_terminal_uuid": "terminal-start", + "end_element_uuid": "device-end", + "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(1, report["collision_warnings"]) + self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) + self.assertEqual("N4111", report["collision_samples"][0]["wire_label"]) + self.assertEqual("device-start", report["collision_samples"][0]["start_element_uuid"]) + self.assertEqual("device-end", report["collision_samples"][0]["end_element_uuid"]) + self.assertEqual( + "N4111: terminal-start -> terminal-end (CollisionWarning)", + report["collision_samples"][0]["wire_object_label"], + ) + self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) + self.assertEqual( + "device-obstacle", + report["collision_samples"][0]["obstacle_element_uuid"], + ) + self.assertEqual( + "instance-obstacle", + report["collision_samples"][0]["obstacle_instance_id"], + ) + self.assertEqual( + "third_party_device_collision", + report["collision_samples"][0]["collision_relation"], + ) + self.assertEqual("HardIntersection", report["collision_samples"][0]["collision_kind"]) + self.assertEqual({"x": 0.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_start"]) + self.assertEqual({"x": 100.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_end"]) + self.assertEqual(40.0, report["collision_samples"][0]["obstacle_bbox"]["xmin"]) + self.assertEqual(35.0, report["collision_samples"][0]["collision_bbox"]["xmin"]) + self.assertEqual("Middle Obstacle", report["routes"][0]["collision_samples"][0]["obstacle_label"]) + self.assertEqual( + "device-obstacle", + report["routes"][0]["collision_samples"][0]["obstacle_element_uuid"], + ) + self.assertEqual( + "instance-obstacle", + report["routes"][0]["collision_samples"][0]["obstacle_instance_id"], + ) + self.assertEqual( + "N4111: terminal-start -> terminal-end (CollisionWarning)", + report["routes"][0]["collision_samples"][0]["wire_object_label"], + ) + self.assertEqual( + "device-start", + report["routes"][0]["collision_samples"][0]["start_element_uuid"], + ) + self.assertEqual( + "device-end", + report["routes"][0]["collision_samples"][0]["end_element_uuid"], + ) + self.assertEqual( + "third_party_device_collision", + report["routes"][0]["collision_samples"][0]["collision_relation"], + ) + self.assertEqual(["QET Route Carrier"], report["collision_samples"][0]["route_source_labels"]) + self.assertIn("碰撞示例", message) + self.assertIn("路径 QET Route Carrier", message) + self.assertIn("Middle Obstacle", message) + + def test_route_eplan_connections_report_calls_out_local_unbound_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, + "LocalTerminal", + "local:instance-1:p1", + app.Vector(0, 0, 0), + ) + payload = { + "wires": [ + { + "wire_id": "wire-1", + "start_terminal_uuid": "qet-terminal-start", + "end_terminal_uuid": "qet-terminal-end", } - ], + ] } + report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message) + self.assertEqual(0, report["routed"]) + self.assertEqual(1, report["available_terminals"]) + self.assertEqual(1, report["local_terminals"]) + self.assertIn("端子匹配失败", message) + self.assertIn("local:", message) - def test_route_report_source_sample_skips_bridge_segments(self): + def test_route_eplan_connections_report_includes_network_and_first_error(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { + "total_wires": 2, "routed": 1, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ + "collision_warnings": 1, + "skipped_missing_terminal": 1, + "prepared_layout": { + "wire_duct_carriers": 2, + "surface_carriers": 4, + "terminal_access_carriers": 6, + }, + "missing_endpoint_samples": [ { - "wire_label": "N4111", - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, - {"is_bridge": True, "carrier": {"kind": "WireDuct", "source_label": "虚拟桥接"}}, - {"carrier": {"kind": "UserPath", "source_label": "用户路径B"}}, - ] - }, + "start_terminal_uuid": "terminal-a", + "end_terminal_uuid": "terminal-b", } ], + "errors": ["没有可用的线槽/路由路径网络"], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径示例:导线 N4111 经过 线槽A、用户路径B。", message) - self.assertNotIn("虚拟桥接", message) + self.assertIn("routed=1", message) + self.assertIn("线槽路径 2 条", message) + self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) + self.assertIn("缺失示例:terminal-a -> terminal-b", message) - def test_route_track_segment_keys_skip_bridge_segments(self): + def test_route_eplan_connections_report_includes_current_route_network_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - route_track = { - "segments": [ - { - "from_key": [0, 0, 0], - "to_key": [100, 0, 0], - "carrier": {"name": "WireDuctA"}, - }, - { - "is_bridge": True, - "from_key": [100, 0, 0], - "to_key": [100, 10, 0], - "carrier": {"name": "VirtualBridge"}, - }, - ] + report = { + "total_wires": 2, + "routed": 2, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "prepared_layout": { + "wire_duct_carriers": 0, + "surface_carriers": 0, + "terminal_access_carriers": 4, + }, + "route_network_carrier_kind_counts": { + "WireDuct": 3, + "UserPath": 2, + "TerminalAccess": 4, + }, } - keys = auto_routing._route_track_segment_keys(route_track) + message = auto_routing.format_eplan_connection_route_report(report) - self.assertEqual(1, len(keys)) - self.assertEqual("WireDuctA", keys[0][0]) + self.assertIn("布线布局空间:线槽路径 0 条,布线面 0 条,端子接入 4 条。", message) + self.assertIn("当前路径网络:线槽路径 3 条,用户路径 2 条,端子接入 4 条。", message) - def test_route_quality_warning_ignores_bridge_only_routing_range(self): + def test_route_eplan_connections_report_calls_out_missing_device_when_some_routes_succeed(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, + "total_wires": 3, + "routed": 2, "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ { - "wire_label": "N4111", - "route_track": { - "carrier_kinds": {"RoutingRange": 1}, - "segments": [ - { - "is_bridge": True, - "carrier": {"kind": "RoutingRange", "source_label": "虚拟布线面桥接"}, - } - ], - }, + "wire_uuid": "wire-missing-device", + "wire_label": "F6", + "start_found": False, + "start_terminal_uuid": "device-a:terminal-as", + "start_element_uuid": "device-a", + "start_terminal_display": "as", + "start_missing_endpoint_reason_code": "device_not_in_3d_scene", + "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", + "end_found": True, } ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertNotIn("路径质量提示", message) + self.assertIn("routed=2", message) + self.assertIn("设备未在当前 FreeCAD 场景中找到", message) + self.assertIn("缺失示例:导线 F6", message) - def test_route_report_includes_network_bridge_and_blocked_segment_counts(self): + def test_route_eplan_connections_report_tolerates_malformed_total_wires(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, + "total_wires": "not-a-number", + "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 0, - "routes": [ - { - "network": { - "bridged_segments": 1, - "blocked_segments": 2, - }, - } - ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径,避障屏蔽 2 段。", message) + self.assertIn("批量生成布线连接完成", message) + self.assertIn("没有导线任务", message) - def test_route_report_prefers_route_track_bridged_segment_count(self): + def test_route_eplan_connections_report_calls_out_missing_route_network(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, + "total_wires": 3, + "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 0, - "routes": [ - { - "network": { - "bridged_segments": 3, - }, - "route_track": { - "bridged_segments": 1, - }, - } - ], + "skipped_missing_route_network": 3, + "route_status_counts": { + "MissingRouteNetwork": 3, + }, } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径。", message) - self.assertNotIn("自动桥接 3 段", message) + self.assertIn("缺少布线路径网络 3 条", message) + self.assertIn("缺少或未连通布线路径网络", message) + self.assertIn("是否已生成 carrier", message) + self.assertIn("路径约束是否过严", message) - def test_route_report_includes_parallel_lane_summary(self): + def test_route_eplan_connections_report_calls_out_zero_routed_after_attempt(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 2, + "total_wires": 75, + "routed": 0, "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ - {"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": 0.0}}, - {"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": -10.0}}, - ], + "skipped_missing_terminal": 4, + "route_network_segments": 1828, + "route_network_carriers": 477, + "route_network_nodes": 706, + "route_status_counts": { + "Error": 71, + "MissingTerminal": 4, + }, + "errors": ["module 'RoutingNetwork' has no attribute 'collect_route_constraint_options'"], } + issue_codes = auto_routing._routing_connection_batch_issue_codes(report) + payload = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("并行错位:最大 lane 2,间距 10.0 mm,最大偏移 30.0 mm。", message) + self.assertIn("no_routed_connections", issue_codes) + self.assertIn("no_routed_connections", payload["issue_codes"]) + self.assertIn("未生成有效导线", message) + self.assertIn("本次只有路径承载/诊断对象,未生成 RoutedConnection 导线", message) - def test_eplan_connection_lane_offset_is_capped_for_dense_parallel_routes(self): + def test_route_eplan_connections_report_treats_error_status_count_as_routing_error(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "total_wires": 75, + "routed": 0, + "collision_warnings": 0, + "skipped_missing_terminal": 4, + "route_status_counts": { + "Error": 71, + "MissingTerminal": 4, + }, + } - result = auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - route_index=21, - options={"lane_spacing": 10.0, "lane_axis": "y"}, - ) + issue_codes = auto_routing._routing_connection_batch_issue_codes(report) + payload = auto_routing._compact_routing_connection_batch_report(report) + message = auto_routing.format_eplan_connection_route_report(report) - self.assertEqual(30.0, result["lane"]["offset_mm"]) - self.assertLessEqual( - max(abs(point.y) for point in result["points"]), - 30.0, - ) + self.assertIn("routing_errors", issue_codes) + self.assertIn("routing_errors", payload["issue_codes"]) + self.assertIn("错误 71 条", message) - def test_route_report_includes_replaced_routed_connection_count(self): + def test_route_eplan_connections_report_includes_missing_route_network_sample(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, + "total_wires": 1, + "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 0, - "replaced_routed_connections": 2, + "skipped_missing_route_network": 1, + "missing_route_network_samples": [ + { + "wire_uuid": "wire-1", + "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", + "error": "没有可用的布线路径网络:起点和终点无法连通", + } + ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("已替换旧布线连接:2 条。", message) + self.assertIn("缺路径网络示例:导线 N4111", message) + self.assertIn("QF1/A1 (terminal-start) -> KM1/13 (terminal-end)", message) + self.assertIn("原因:没有可用的布线路径网络:起点和终点无法连通", message) - def test_route_report_includes_hidden_route_carrier_count(self): + def test_route_eplan_connections_report_includes_readable_missing_endpoint_labels(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 1, + "routed": 0, "collision_warnings": 0, - "skipped_missing_terminal": 0, - "hidden_route_carriers": 3, + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ + { + "start_terminal_uuid": "terminal-a", + "start_element_uuid": "device-a", + "start_terminal_display": "A1", + "end_terminal_uuid": "terminal-b", + "end_element_uuid": "device-b", + "end_terminal_display": "B1", + } + ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("已隐藏走线路径辅助对象:3 条。", message) + self.assertIn("缺失示例:device-a/A1 (terminal-a) -> device-b/B1 (terminal-b)", message) - def test_route_report_warns_when_routes_use_surface_or_auxiliary_paths(self): + def test_route_eplan_connections_report_includes_wire_object_label_for_missing_endpoint(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 2, + "routed": 0, "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ - { - "wire_label": "N1", - "route_track": { - "carrier_kinds": { - "TerminalAccess": 2, - "WireDuct": 1, - "RoutingRange": 2, - }, - }, - }, + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ { - "wire_label": "N2", - "route_track": { - "carrier_kinds": { - "TerminalAccess": 2, - "WireDuct": 3, - }, - }, - }, + "wire_uuid": "wire-missing", + "wire_label": "N-MISS", + "wire_object_label": "N-MISS: QF1/A1 -> KM1/13 (MissingTerminal)", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + } ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("路径质量提示:1 条导线使用布线面/辅助路径", message) - self.assertIn("示例 N1 使用布线面。", message) + self.assertIn( + "缺失示例:导线 N-MISS: QF1/A1 -> KM1/13 (MissingTerminal),terminal-start -> terminal-missing", + message, + ) - def test_route_report_warns_when_parallel_lanes_exceed_track_capacity(self): + def test_route_eplan_connections_report_identifies_which_endpoint_is_missing(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 3, + "routed": 0, "collision_warnings": 0, - "skipped_missing_terminal": 0, - "routes": [ + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ { - "lane": {"index": 2, "spacing_mm": 10.0}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "capacity": 2}}, - {"carrier": {"kind": "WireDuct", "capacity": 4}}, - ] - }, + "start_terminal_uuid": "terminal-start", + "start_found": True, + "end_terminal_uuid": "terminal-missing", + "end_found": False, } ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("容量提示:最大并行线数 3,路径最小容量 2。", message) + self.assertIn("缺失示例:terminal-start -> terminal-missing", message) + self.assertIn("缺失:终点", message) - def test_route_report_ignores_bridge_segments_for_capacity_pressure(self): + def test_route_eplan_connections_report_uses_wire_object_label_for_collision_sample(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 3, - "collision_warnings": 0, + "routed": 1, + "collision_warnings": 1, "skipped_missing_terminal": 0, - "routes": [ + "collision_samples": [ { - "lane": {"index": 2, "spacing_mm": 10.0}, - "route_track": { - "segments": [ - {"is_bridge": True, "carrier": {"kind": "UserPath", "capacity": 1}}, - {"carrier": {"kind": "WireDuct", "capacity": 4}}, - ] - }, + "wire_uuid": "wire-collision", + "wire_label": "N-COL", + "wire_object_label": "N-COL: QF1/A1 -> KM1/13 (CollisionWarning)", + "obstacle_label": "柜体侧板", + "collision_kind": "HardIntersection", } ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertNotIn("容量提示", message) + self.assertIn( + "碰撞示例:导线 N-COL: QF1/A1 -> KM1/13 (CollisionWarning) 碰到 柜体侧板", + message, + ) - def test_route_report_includes_entry_candidate_rank_when_route_uses_fallback_entry(self): + def test_route_eplan_connections_report_calls_out_clearance_collision_kind(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, - "collision_warnings": 0, + "collision_warnings": 1, "skipped_missing_terminal": 0, - "routes": [ + "collision_samples": [ { - "wire_label": "N1", - "network": { - "entry_candidate_rank": 3, - "exit_candidate_rank": 1, - "entry_candidate_score": 125.0, - }, + "wire_label": "N4111", + "obstacle_label": "柜体侧板", + "collision_kind": "ClearanceWarning", } ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("接入候选", message) - self.assertIn("起点第 3 个", message) + self.assertIn("安全间隙", message) + self.assertIn("柜体侧板", message) - def test_route_report_warns_when_network_entry_distance_is_long(self): + def test_route_report_includes_collision_kind_summary(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_label": "N1", - "network": { - "entry_distance": 125.0, - "exit_distance": 20.0, - }, + "collision_samples": [ + {"collision_kind": "HardIntersection", "obstacle_label": "设备A"}, + ], }, { - "wire_label": "N2", - "network": { - "entry_distance": 20.0, - "exit_distance": 150.0, - }, + "collision_samples": [ + {"collision_kind": "ClearanceWarning", "obstacle_label": "设备B"}, + ], }, ], } message = auto_routing.format_eplan_connection_route_report(report) - self.assertIn("接入距离提示:2 条导线", message) - self.assertIn("示例导线 N1", message) - self.assertIn("起点接入 125.0 mm", message) + self.assertIn("碰撞分类:硬碰撞 1 处,安全间隙 1 处。", message) - def test_route_report_capacity_pressure_is_checked_per_route(self): + def test_route_report_includes_top_collision_obstacles(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { - "routed": 2, - "collision_warnings": 0, + "routed": 3, + "collision_warnings": 3, "skipped_missing_terminal": 0, "routes": [ { - "lane": {"index": 2, "spacing_mm": 10.0}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "capacity": 4}}, - ] - }, + "collision_samples": [ + { + "collision_kind": "HardIntersection", + "obstacle_label": "设备A", + "obstacle_name": "DeviceA", + "obstacle_element_uuid": "device-a", + "obstacle_instance_id": "instance-a", + "collision_relation": "third_party_device_collision", + "obstacle_parent_labels": ["安装板A"], + }, + { + "collision_kind": "ClearanceWarning", + "obstacle_label": "设备A", + "obstacle_name": "DeviceA", + "obstacle_element_uuid": "device-a", + "obstacle_instance_id": "instance-a", + "collision_relation": "third_party_device_collision", + "obstacle_parent_labels": ["安装板A"], + }, + ], }, { - "lane": {"index": 0, "spacing_mm": 10.0}, - "route_track": { - "segments": [ - {"carrier": {"kind": "WireDuct", "capacity": 1}}, - ] - }, + "collision_samples": [ + {"collision_kind": "HardIntersection", "obstacle_label": "支架B"}, + ], }, ], } message = auto_routing.format_eplan_connection_route_report(report) + top_obstacles = auto_routing._top_collision_obstacles(report) - self.assertNotIn("容量提示", message) + self.assertEqual("device-a", top_obstacles[0]["element_uuid"]) + self.assertEqual("instance-a", top_obstacles[0]["instance_id"]) + self.assertEqual( + {"third_party_device_collision": 2}, + top_obstacles[0]["collision_relation_counts"], + ) + self.assertIn("碰撞关系:第三方设备/布局 2 处。", message) + self.assertIn("后续处理:优先对第三方设备/布局碰撞做局部二次避障", message) + self.assertIn("碰撞高发对象:设备A(安装板A) 2 处,支架B 1 处。", message) + self.assertIn( + "碰撞处理建议:设备A:疑似设备/安装区域碰撞,优先补柜内路径或调整装配;支架B:疑似柜体/门板/支架结构,确认可穿越后标记忽略碰撞。", + message, + ) - def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self): + def test_route_eplan_connections_report_ignores_non_numeric_status_counts(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] - doc = FakeDocument() - terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-1", - "wire_label": "N4111", - "wire_style_id": "42", - "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-end", - } - ], + _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() + report = { + "routed": 1, + "collision_warnings": 0, + "skipped_missing_terminal": 0, + "route_status_counts": { + "Routed": "1", + "ExternalStatus": "not-a-number", + }, } - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"lane_spacing": 12.0, "lane_axis": "y"}, - ) - route = report["routes"][0] + message = auto_routing.format_eplan_connection_route_report(report) - self.assertEqual("wire-1", route["wire_uuid"]) - self.assertEqual("N4111", route["wire_label"]) - self.assertEqual("42", route["wire_style_id"]) - self.assertEqual("terminal-start", route["start_terminal_uuid"]) - self.assertEqual("terminal-end", route["end_terminal_uuid"]) - self.assertEqual(0, route["lane"]["index"]) - self.assertEqual("network-dijkstra-v1", route["algorithm"]) - self.assertEqual(1, route["network"]["carriers"]) - self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) + self.assertIn("正常 1 条", message) + self.assertNotIn("ExternalStatus", message) - def test_route_eplan_connections_can_skip_nearer_isolated_entry_network(self): + def test_routing_preflight_reports_missing_route_network_and_style_database(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, 1, 20), app.Vector(5, 1, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], - project_uuid="project-1", - kind="WireDuct", - ) payload = { "project_uuid": "project-1", + "wire_style_database_path": "D:/missing/project-local.db", "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", + "wire_style_id": "42", } ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertEqual(1, report["routed"]) - self.assertEqual(0, len(report["errors"])) - route = report["routes"][0] - self.assertEqual("network-dijkstra-v1", route["algorithm"]) - self.assertGreater(route["network"]["entry_distance"], 1.0) - self.assertGreater(route["network"]["entry_candidate_rank"], 1) + self.assertFalse(report["ok"]) + self.assertEqual(1, report["total_wires"]) + self.assertEqual(2, report["available_terminals"]) + self.assertEqual(0, report["route_network_segments"]) + self.assertEqual("Missing", report["wire_style_database"]["status"]) + self.assertIn("no_route_network", report["issue_codes"]) + self.assertIn("wire_style_database_missing", report["issue_codes"]) + self.assertIn("路径网络:0 段", message) + self.assertIn("导线样式库:文件不存在", message) - def test_route_eplan_connections_report_includes_routing_path_network_diagnostic(self): + def test_routing_preflight_report_identifies_qet_session_payload_source(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(100, 0, 20)], - project_uuid="project-1", - kind="RoutingRange", - ) payload = { "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-range-only", - "wire_label": "N-RANGE", + "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } - report = auto_routing.route_eplan_connections( - doc, - payload=payload, - options={"hide_route_carriers_after_route": False}, - project_uuid="project-1", - ) - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertEqual(1, report["routed"]) - self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) - self.assertIn( - "routing_range_only_network", - report["routing_path_network_diagnostic"]["issue_codes"], - ) - self.assertIn("路径网络检查提示", message) - self.assertIn("仅使用布线面兜底", message) + self.assertEqual("payload", report["source"]) + self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, report["runtime_version"]) + self.assertIn("导线来源:QET 会话交换数据", message) + self.assertIn("运行版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), message) - def test_route_eplan_connections_preserves_endpoint_metadata_on_routed_wire(self): + def test_routing_preflight_missing_endpoint_sample_includes_instance_details(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(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) payload = { "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-1", - "start_element_uuid": "device-a", - "start_terminal_uuid": "terminal-start", + "wire_id": "wire-missing", + "wire_label": "N-MISS", + "start_terminal_uuid": "terminal-missing", + "start_element_uuid": "device-missing", + "start_instance_id": "instance-missing", "start_terminal_display": "A1", - "end_element_uuid": "device-b", "end_terminal_uuid": "terminal-end", - "end_terminal_display": "B1", + "end_element_uuid": "device-end", + "end_instance_id": "instance-end", + "end_terminal_display": "13", } - ], - } - - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - routed_group = doc.getObject("QETWiring_04_Routed") - wire = routed_group.Group[0] - diagnostics = json.loads(wire.QetRouteDiagnosticsJson) - - self.assertEqual("device-a", wire.QetStartElementUuid) - self.assertEqual("A1", wire.QetStartTerminalDisplay) - self.assertEqual("device-b", wire.QetEndElementUuid) - self.assertEqual("B1", wire.QetEndTerminalDisplay) - self.assertEqual("device-a", report["routes"][0]["start_element_uuid"]) - self.assertEqual("B1", report["routes"][0]["end_terminal_display"]) - self.assertEqual("A1", diagnostics["endpoint_metadata"]["start_terminal_display"]) + ], + } - def test_route_eplan_connection_tasks_preserve_task_endpoint_labels_on_routed_wire(self): + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + sample = report["missing_endpoint_samples"][0] + + self.assertEqual("device-missing", sample["start_element_uuid"]) + self.assertEqual("instance-missing", sample["start_instance_id"]) + self.assertEqual("A1", sample["start_terminal_display"]) + self.assertEqual(0, sample["start_element_terminal_count"]) + self.assertEqual([], sample["start_element_terminal_samples"]) + self.assertEqual(0, sample["start_instance_terminal_count"]) + self.assertEqual([], sample["start_instance_terminal_samples"]) + self.assertEqual("device_not_in_3d_scene", sample["start_missing_endpoint_reason_code"]) + self.assertEqual("该 2D 设备未在 FreeCAD 场景中找到", sample["start_missing_endpoint_reason_label"]) + self.assertIn("device-missing/A1 (terminal-missing)", message) + self.assertIn("起点 element=device-missing, instance=instance-missing, terminal=A1", message) + self.assertIn("FreeCAD同设备端子=0", message) + self.assertIn("FreeCAD同实例端子=0", message) + self.assertIn("原因=该 2D 设备未在 FreeCAD 场景中找到", message) + + def test_routing_preflight_reports_missing_runtime_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)) _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", - ) - task = wiring_objects.create_wire_task( - doc, - "project-1", - "wire-1", - "N4111", - "terminal-start", - "terminal-end", - "instance-a", - "instance-b", - ) - terminal_objects.ensure_string_property(task, "QetStartDeviceLabel", "QET Wiring", "", "QF1") - terminal_objects.ensure_string_property(task, "QetEndDeviceLabel", "QET Wiring", "", "X1") - terminal_objects.ensure_string_property(task, "QetEndpointLabel", "QET Wiring", "", "QF1:A1 -> X1:B1") - - report = auto_routing.route_eplan_connection_tasks(doc) - routed_group = doc.getObject("QETWiring_04_Routed") - wire = routed_group.Group[0] - diagnostics = json.loads(wire.QetRouteDiagnosticsJson) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + collector = routing_network.collect_route_constraint_options + delattr(routing_network, "collect_route_constraint_options") + try: + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + finally: + routing_network.collect_route_constraint_options = collector - self.assertEqual("QF1", wire.QetStartDeviceLabel) - self.assertEqual("X1", wire.QetEndDeviceLabel) - self.assertEqual("QF1:A1 -> X1:B1", wire.QetEndpointLabel) - self.assertEqual("QF1:A1 -> X1:B1", report["routes"][0]["endpoint_label"]) - self.assertEqual("QF1", diagnostics["endpoint_metadata"]["start_device_label"]) + self.assertFalse(report["ok"]) + self.assertIn("runtime_route_constraint_collector_missing", report["issue_codes"]) + self.assertFalse(report["runtime_capabilities"]["route_constraint_collector"]) + self.assertIn("运行模块能力", message) + self.assertIn("路径约束收集函数缺失", message) - def test_route_eplan_connections_records_wire_identity_for_errors(self): + def test_routing_preflight_writes_compact_diagnostic_object(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, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) payload = { "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-bad", - "wire_label": "N500", + "wire_id": "wire-1", + "wire_label": "N4111", "start_terminal_uuid": "terminal-start", - "end_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-missing", + "wire_style_id": "42", } ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + first = auto_routing.write_routing_preflight_diagnostic(doc, report) + second = auto_routing.write_routing_preflight_diagnostic(doc, report) + diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") + diagnostic_payload = json.loads(second.QetDiagnosticJson) - self.assertEqual(1, len(report["errors"])) - self.assertIn("error_samples", report) - self.assertEqual("wire-bad", report["error_samples"][0]["wire_uuid"]) - self.assertEqual("N500", report["error_samples"][0]["wire_label"]) - self.assertEqual("terminal-start", report["error_samples"][0]["start_terminal_uuid"]) - self.assertEqual("terminal-start", report["error_samples"][0]["end_terminal_uuid"]) - self.assertIn("different", report["error_samples"][0]["error"]) + self.assertIsNotNone(first) + self.assertIsNotNone(second) + self.assertIsNot(first, second) + self.assertEqual(1, len(diagnostic_group.Group)) + self.assertEqual("RoutingPreflight", diagnostic_group.Group[0].QetDiagnosticKind) + self.assertEqual("project-1", diagnostic_group.Group[0].QetProjectUuid) + self.assertFalse(diagnostic_group.Group[0].QetDiagnosticOk) + self.assertIn("missing_endpoints", diagnostic_group.Group[0].QetDiagnosticIssueCodes) + self.assertIn("缺失端点", diagnostic_group.Group[0].QetDiagnosticIssueLabels) + self.assertIn("布线准备度:未通过", diagnostic_group.Group[0].QetDiagnosticMessage) + self.assertEqual("project-1", diagnostic_payload["project_uuid"]) + self.assertEqual(1, diagnostic_payload["total_wires"]) + self.assertEqual(1, diagnostic_payload["available_terminals"]) + self.assertIn("missing_endpoints", diagnostic_payload["issue_codes"]) + self.assertEqual(1, diagnostic_payload["missing_endpoint_uuid_count"]) + self.assertEqual("terminal-missing", diagnostic_payload["missing_endpoint_uuids"][0]) + self.assertIn( + "端点缺失示例:导线 N4111,terminal-start -> terminal-missing", + message, + ) + self.assertIn("routing_sources", diagnostic_payload) + self.assertIn("routing_boundaries", diagnostic_payload) + self.assertIn("wire_style_database", diagnostic_payload) + self.assertIn("wire_style", diagnostic_payload) + self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, diagnostic_payload["runtime_version"]) - def test_route_eplan_connections_report_includes_readable_error_sample(self): + def test_routing_preflight_reports_obstacle_mode_summary(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, 20), app.Vector(100, 0, 20)], - project_uuid="project-1", - kind="WireDuct", - ) + ignored = doc.addObject("Part::Feature", "IgnoredBracket") + ignored.Label = "忽略支架" + ignored.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) + ignored.QetRoutingObstacleMode = "PassThrough" payload = { "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-bad", - "wire_label": "N500", - "start_element_uuid": "device-a", + "wire_id": "wire-1", + "wire_label": "N4111", "start_terminal_uuid": "terminal-start", - "start_terminal_display": "A1", - "end_element_uuid": "device-a", - "end_terminal_uuid": "terminal-start", - "end_terminal_display": "A1", + "end_terminal_uuid": "terminal-missing", } ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertEqual("device-a", report["error_samples"][0]["start_element_uuid"]) - self.assertIn("错误示例:导线 N500", message) - self.assertIn("device-a/A1 (terminal-start) -> device-a/A1 (terminal-start)", message) + self.assertEqual(1, report["routing_obstacle_modes"]["PassThrough"]["count"]) + self.assertEqual("忽略支架", report["routing_obstacle_modes"]["PassThrough"]["samples"][0]["label"]) + self.assertIn("忽略碰撞对象:1", message) - def test_route_eplan_connections_counts_route_statuses_for_summary(self): + def test_collect_routing_diagnostic_summary_merges_latest_diagnostic_objects(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "RouteStart", "route-start", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "RouteEnd", "route-end", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "CollisionStart", "collision-start", app.Vector(0, 100, 0)) - _terminal(doc, terminal_objects, "CollisionEnd", "collision-end", app.Vector(100, 100, 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, 100, 100), app.Vector(100, 100, 100)], - project_uuid="project-1", - kind="WireDuct", - ) - obstacle = doc.addObject("Part::Feature", "CollisionObstacle") - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 90, 110, 90, 110)) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-ok", - "start_terminal_uuid": "route-start", - "end_terminal_uuid": "route-end", - }, + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + + for kind, ok, message, payload in ( + ( + "RoutingPreflight", + False, + "布线准备度:未通过。", { - "wire_id": "wire-collision", - "start_terminal_uuid": "collision-start", - "end_terminal_uuid": "collision-end", + "issue_codes": ["missing_endpoints"], + "total_wires": 2, + "runtime_version": "preflight-version", }, + ), + ( + "RoutingPathNetwork", + False, + "布线路径网络检查发现 1 类问题。", + {"issue_codes": ["unconnected_terminals"], "summary": {"segments": 4}}, + ), + ( + "RoutingConnectionBatch", + True, + "自动布线完成:已生成 2 条。", { - "wire_id": "wire-error", - "start_terminal_uuid": "route-start", - "end_terminal_uuid": "route-start", + "issue_codes": [], + "routed": 2, + "route_path_usage": {"main_path_routes": 1, "fallback_routes": 1}, + "top_collision_obstacles": [ + {"label": "设备A", "count": 2, "parent_labels": ["安装板A"]} + ], + "runtime_version": auto_routing.AUTO_ROUTING_RUNTIME_VERSION, }, - ], - } + ), + ): + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_{0}".format(kind)) + diagnostic.QetDiagnosticKind = kind + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = ok + diagnostic.QetDiagnosticMessage = message + diagnostic.QetDiagnosticJson = json.dumps(payload, ensure_ascii=False) + diagnostic_group.addObject(diagnostic) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertFalse(summary["ok"]) + self.assertEqual("project-1", summary["project_uuid"]) + self.assertEqual(3, summary["diagnostic_count"]) + self.assertEqual( + ["missing_endpoints", "unconnected_terminals"], + summary["issue_codes"], + ) + self.assertEqual([], summary["missing_diagnostic_kinds"]) + self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, summary["runtime_version"]) + self.assertEqual(2, summary["diagnostics"]["RoutingConnectionBatch"]["payload"]["routed"]) + self.assertEqual( + {"main_path_routes": 1, "fallback_routes": 1}, + summary["batch_route_path_usage"], + ) + self.assertEqual( + [{"label": "设备A", "count": 2, "parent_labels": ["安装板A"]}], + summary["batch_top_collision_obstacles"], + ) + self.assertIn("汇总诊断:未通过", message) + self.assertIn("运行版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), message) + self.assertIn("路径采用:线槽/主路径 1 条,布线面/辅助路径 1 条。", message) + self.assertIn("碰撞高发对象:设备A(安装板A) 2 处。", message) + self.assertIn("缺失端点", message) + self.assertIn("端子未接入", message) - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"avoid_obstacles": False}, + def test_write_routing_diagnostic_summary_replaces_previous_summary_object(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") + diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPreflight") + diagnostic.QetDiagnosticKind = "RoutingPreflight" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "布线准备度:未通过。" + diagnostic.QetDiagnosticJson = json.dumps( + {"issue_codes": ["missing_endpoints"], "total_wires": 2}, + ensure_ascii=False, ) - message = auto_routing.format_eplan_connection_route_report(report) + diagnostic_group.addObject(diagnostic) - self.assertEqual(1, report["route_status_counts"]["Routed"]) - self.assertEqual(1, report["route_status_counts"]["CollisionWarning"]) - self.assertEqual(1, report["route_status_counts"]["Error"]) - self.assertIn("结果状态", message) - self.assertIn("正常 1 条", message) + first = auto_routing.write_routing_diagnostic_summary(doc) + second = auto_routing.write_routing_diagnostic_summary(doc) + summary_objects = [ + item + for item in diagnostic_group.Group + if getattr(item, "QetDiagnosticKind", "") == "RoutingDiagnosticSummary" + ] + payload = json.loads(second.QetDiagnosticJson) + + self.assertIsNotNone(first) + self.assertIsNotNone(second) + self.assertIsNot(first, second) + self.assertEqual(1, len(summary_objects)) + self.assertEqual("RoutingDiagnosticSummary", second.QetDiagnosticKind) + self.assertEqual("project-1", second.QetProjectUuid) + self.assertFalse(second.QetDiagnosticOk) + self.assertEqual("missing_endpoints", second.QetDiagnosticIssueCodes) + self.assertEqual("缺失端点", second.QetDiagnosticIssueLabels) + self.assertIn("汇总诊断:未通过", second.QetDiagnosticMessage) + self.assertEqual(["missing_endpoints"], payload["issue_codes"]) + self.assertIn("RoutingPathNetwork", payload["missing_diagnostic_kinds"]) + self.assertIn("RoutingConnectionBatch", payload["missing_diagnostic_kinds"]) + + def test_collect_routing_diagnostic_summary_falls_back_to_issue_code_property(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = False + diagnostic.QetDiagnosticIssueCodes = "missing_terminals, collision_warnings" + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = "{broken-json" + diagnostic_group.addObject(diagnostic) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + + self.assertIn("missing_terminals", summary["issue_codes"]) + self.assertIn("collision_warnings", summary["issue_codes"]) + self.assertIn("diagnostic_json_invalid", summary["issue_codes"]) + + def test_collect_routing_diagnostic_summary_accepts_batch_as_final_diagnostic(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = True + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成:routed=2, collision_warnings=0, missing_terminals=0" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": [], + "routed": 2, + "skipped_missing_terminal": 0, + "collision_warnings": 0, + "route_path_usage": {"main_path_routes": 2, "fallback_routes": 0}, + "routing_path_network_diagnostic": {"issue_codes": [], "summary": {"segments": 4}}, + "runtime_version": auto_routing.AUTO_ROUTING_RUNTIME_VERSION, + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertTrue(summary["ok"]) + self.assertEqual([], summary["missing_diagnostic_kinds"]) + self.assertNotIn("未生成", message) + self.assertIn("汇总诊断:通过", message) + self.assertIn("路径采用:线槽/主路径 2 条,布线面/辅助路径 0 条。", message) + + def test_collect_routing_diagnostic_summary_reports_empty_diagnostic_json(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = False + diagnostic.QetDiagnosticIssueCodes = "" + diagnostic.QetDiagnosticMessage = "" + diagnostic.QetDiagnosticJson = "" + diagnostic_group.addObject(diagnostic) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertIn("diagnostic_json_empty", summary["issue_codes"]) + self.assertIn("诊断 JSON 为空", summary["issue_labels"]) + self.assertIn("诊断 JSON 为空", message) + + def test_collect_routing_diagnostic_summary_reports_routed_wires_missing_diagnostics(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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") + wire = doc.addObject("Part::Feature", "QETRoutedConnection_legacy") + wire.Label = "N-OLD: A1 -> B1" + wire.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + ] + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-old" + wire.QetStartTerminalUuid = "terminal-a" + wire.QetEndTerminalUuid = "terminal-b" + wire.QetRouteDiagnosticsJson = "" + routed_group.addObject(wire) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertIn("routed_wire_diagnostics_missing", summary["issue_codes"]) + self.assertEqual(1, summary["routed_wire_diagnostic_gaps"]["count"]) + self.assertEqual("N-OLD: A1 -> B1", summary["routed_wire_diagnostic_gaps"]["samples"][0]["label"]) + self.assertIn("导线诊断缺失", summary["issue_labels"]) + self.assertIn("导线诊断缺失:1 条", message) + self.assertIn("N-OLD: A1 -> B1", message) + + def test_collect_routing_diagnostic_summary_reports_invalid_routed_wire_diagnostics(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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") + wire = doc.addObject("Part::Feature", "QETRoutedConnection_invalid_diag") + wire.Label = "N-BAD: A1 -> B1" + wire.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + ] + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-bad" + wire.QetStartTerminalUuid = "terminal-a" + wire.QetEndTerminalUuid = "terminal-b" + wire.QetRouteDiagnosticsJson = "{broken-json" + routed_group.addObject(wire) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertIn("routed_wire_diagnostics_invalid", summary["issue_codes"]) + self.assertEqual(1, summary["routed_wire_diagnostic_gaps"]["invalid_count"]) + self.assertEqual("N-BAD: A1 -> B1", summary["routed_wire_diagnostic_gaps"]["invalid_samples"][0]["label"]) + self.assertIn("导线诊断 JSON 无效", summary["issue_labels"]) + self.assertIn("导线诊断 JSON 无效:1 条", message) + self.assertIn("N-BAD: A1 -> B1", message) + + def test_collect_routing_diagnostic_summary_reports_main_path_detour_missing_details(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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") + wire_a = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_a") + wire_a.Label = "N-A: A1 -> B1" + wire_a.RouteType = "RoutedConnection" + wire_a.QetWireUuid = "wire-a" + wire_a.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + "QetRouteTrackJson", + ] + wire_a.QetStartTerminalUuid = "terminal-a" + wire_a.QetEndTerminalUuid = "terminal-b" + wire_a.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + wire_a.QetRouteDiagnosticsJson = json.dumps( + { + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["RoutingRange"], + "rejected_fallback_labels": ["安装板布线面", "辅助路径A"], + } + }, + ensure_ascii=False, + ) + wire_a.QetRouteTrackJson = json.dumps( + { + "segments": [ + {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}}, + ] + }, + ensure_ascii=False, + ) + routed_group.addObject(wire_a) + wire_b = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_b") + wire_b.Label = "N-B: A2 -> B2" + wire_b.RouteType = "RoutedConnection" + wire_b.QetWireUuid = "wire-b" + wire_b.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + "QetRouteTrackJson", + ] + wire_b.QetStartTerminalUuid = "terminal-c" + wire_b.QetEndTerminalUuid = "terminal-d" + wire_b.QetRouteIssueCodes = "main_path_detour_missing" + wire_b.QetRouteDiagnosticsJson = json.dumps( + { + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["AuxiliaryPath"], + "rejected_fallback_labels": ["辅助路径A", "门板附近辅助路径"], + } + }, + ensure_ascii=False, + ) + wire_b.QetRouteTrackJson = json.dumps( + { + "segments": [ + {"carrier": {"kind": "UserPath", "source_label": "主路径B"}}, + ] + }, + ensure_ascii=False, + ) + routed_group.addObject(wire_b) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + detour = summary["main_path_detour_missing_summary"] + self.assertEqual(2, detour["wire_count"]) + self.assertEqual(["安装板布线面", "辅助路径A", "门板附近辅助路径"], detour["rejected_fallback_labels"]) + self.assertEqual( + {"安装板布线面": 1, "辅助路径A": 2, "门板附近辅助路径": 1}, + detour["rejected_fallback_label_counts"], + ) + self.assertEqual({"AuxiliaryPath": 1, "RoutingRange": 1}, detour["rejected_fallback_kind_counts"]) + self.assertEqual({"主线槽A": 1, "主路径B": 1}, detour["current_route_source_label_counts"]) + self.assertEqual( + { + "安装板布线面 -> 主线槽A": 1, + "辅助路径A -> 主线槽A": 1, + "辅助路径A -> 主路径B": 1, + "门板附近辅助路径 -> 主路径B": 1, + }, + detour["bridge_pair_counts"], + ) + self.assertEqual(["主线槽A"], detour["samples"][0]["current_route_source_labels"]) + self.assertEqual(["N-A: A1 -> B1", "N-B: A2 -> B2"], [item["label"] for item in detour["samples"]]) + self.assertIn("缺主路径绕行:2 条", message) + self.assertIn("需补路径位置:辅助路径A 2 条、安装板布线面 1 条、门板附近辅助路径 1 条", message) + self.assertIn("辅助路径A -> 主线槽A 1 条", message) + + def test_collect_routing_diagnostic_summary_counts_routed_wire_issue_codes(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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") + long_wire = doc.addObject("Part::Feature", "QETRoutedConnection_long") + long_wire.Label = "N-LONG: A1 -> B1" + long_wire.RouteType = "RoutedConnection" + long_wire.QetWireUuid = "wire-long" + long_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] + long_wire.QetStartTerminalUuid = "terminal-a" + long_wire.QetEndTerminalUuid = "terminal-b" + long_wire.QetRouteIssueCodes = "long_terminal_access" + routed_group.addObject(long_wire) + collision_wire = doc.addObject("Part::Feature", "QETRoutedConnection_collision") + collision_wire.Label = "N-COL: A2 -> B2" + collision_wire.RouteType = "RoutedConnection" + collision_wire.QetWireUuid = "wire-col" + collision_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] + collision_wire.QetStartTerminalUuid = "terminal-c" + collision_wire.QetEndTerminalUuid = "terminal-d" + collision_wire.QetRouteIssueCodes = "collision_warnings, route_capacity_pressure" + routed_group.addObject(collision_wire) + normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_ok") + normal_wire.Label = "N-OK: A3 -> B3" + normal_wire.RouteType = "RoutedConnection" + normal_wire.QetWireUuid = "wire-ok" + normal_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] + normal_wire.QetStartTerminalUuid = "terminal-e" + normal_wire.QetEndTerminalUuid = "terminal-f" + normal_wire.QetRouteIssueCodes = "" + routed_group.addObject(normal_wire) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + issue_summary = summary["routed_wire_issue_summary"] + self.assertEqual(2, issue_summary["issue_wire_count"]) + self.assertEqual(3, issue_summary["total_wire_count"]) + self.assertEqual( + { + "collision_warnings": 1, + "long_terminal_access": 1, + "route_capacity_pressure": 1, + }, + issue_summary["issue_code_counts"], + ) + self.assertEqual("N-LONG: A1 -> B1", issue_summary["samples"][0]["label"]) + self.assertIn("异常导线:2/3 条", message) + self.assertIn("端子接入过长 1 条", message) self.assertIn("碰撞告警 1 条", message) - self.assertIn("错误 1 条", message) - def test_route_eplan_connections_lane_index_is_per_terminal_pair(self): + def test_collect_routing_diagnostic_summary_counts_missing_terminal_reasons(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": ["missing_terminals"], + "skipped_missing_terminal": 4, + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "wire_label": "N-A", + "start_found": False, + "start_terminal_uuid": "terminal-a", + "start_element_uuid": "device-a", + "start_device_label": "设备A", + "start_terminal_display": "A1", + "start_missing_endpoint_reason_code": "no_3d_terminals_for_element", + "start_missing_endpoint_reason_label": "该 2D 设备在 FreeCAD 中没有工程端子", + "end_found": True, + }, + { + "wire_uuid": "wire-b", + "wire_label": "N-B", + "start_found": False, + "start_terminal_uuid": "terminal-b", + "start_element_uuid": "device-a", + "start_device_label": "设备A", + "start_terminal_display": "A2", + "start_missing_endpoint_reason_code": "no_3d_terminals_for_element", + "start_missing_endpoint_reason_label": "该 2D 设备在 FreeCAD 中没有工程端子", + "end_found": False, + "end_terminal_uuid": "terminal-c", + "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", + }, + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + missing_summary = summary["batch_missing_terminal_summary"] + self.assertEqual(4, missing_summary["skipped_missing_terminal"]) + self.assertEqual(2, missing_summary["sample_wire_count"]) + self.assertEqual(3, missing_summary["missing_endpoint_count"]) + self.assertEqual( + { + "no_3d_terminals_for_element": 2, + "terminal_uuid_not_in_element": 1, + }, + missing_summary["reason_code_counts"], + ) + self.assertEqual(2, len(missing_summary["device_groups"])) + self.assertEqual("设备A", missing_summary["device_groups"][0]["device_label"]) + self.assertEqual(2, missing_summary["device_groups"][0]["missing_endpoint_count"]) + self.assertEqual(["A1", "A2"], missing_summary["device_groups"][0]["terminal_displays"]) + self.assertEqual(["terminal-a", "terminal-b"], missing_summary["device_groups"][0]["terminal_uuids"]) + self.assertEqual("设备B", missing_summary["device_groups"][1]["device_label"]) + self.assertIn("缺端子:4 条", message) + self.assertIn("该 2D 设备在 FreeCAD 中没有工程端子 2 处", message) + self.assertIn("需补端子设备:设备A 缺 2 处(A1、A2),设备B 缺 1 处(B1)", message) + + def test_collect_routing_diagnostic_summary_reports_missing_terminal_samples_without_reason_codes(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "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, 100, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 100, 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, 100, 20), app.Vector(100, 100, 20)], - project_uuid="project-1", - kind="WireDuct", + 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.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": ["missing_terminals"], + "skipped_missing_terminal": 2, + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-a", + "end_found": True, + }, + { + "wire_uuid": "wire-b", + "start_found": True, + "end_found": False, + "end_terminal_uuid": "terminal-b", + }, + ], + }, + ensure_ascii=False, ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, - { - "wire_id": "wire-a-repeat", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - ], - } + diagnostic_group.addObject(diagnostic) - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"lane_spacing": 10.0, "lane_axis": "y"}, - ) + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) - self.assertEqual(0, report["routes"][0]["lane"]["index"]) - self.assertEqual(0, report["routes"][1]["lane"]["index"]) - self.assertEqual(1, report["routes"][2]["lane"]["index"]) + missing_summary = summary["batch_missing_terminal_summary"] + self.assertEqual({"missing_device_binding_metadata": 2}, missing_summary["reason_code_counts"]) + self.assertEqual({"导线端点缺少 2D/3D 设备绑定信息": 2}, missing_summary["reason_label_counts"]) + self.assertIn("导线端点缺少 2D/3D 设备绑定信息 2 处", message) + self.assertIn( + "检查 QET 导线端点是否提供 element_uuid 和 terminal_uuid(第一版不要求 start/end_instance_id)", + summary["recommended_actions"], + ) - def test_route_eplan_connections_lane_index_increments_for_shared_route_segments(self): + def test_collect_routing_diagnostic_summary_backfills_missing_endpoint_reason_from_old_batch(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "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)], - project_uuid="project-1", - kind="WireDuct", + 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.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": ["missing_terminals"], + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-a", + "start_element_uuid": "device-a", + "start_instance_id": "instance-a", + "start_terminal_display": "A1", + "end_found": True, + } + ], + }, + ensure_ascii=False, ) - 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", - }, - ], - } + diagnostic_group.addObject(diagnostic) - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"lane_spacing": 10.0, "lane_axis": "y"}, - ) + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) - self.assertEqual(0, report["routes"][0]["lane"]["index"]) - self.assertEqual(1, report["routes"][1]["lane"]["index"]) - routed_group = doc.getObject("QETWiring_04_Routed") - self.assertEqual(2, len(list(getattr(routed_group, "Group", []) or []))) - second_wire = [ - wire - for wire in list(getattr(routed_group, "Group", []) or []) - if getattr(wire, "QetWireUuid", "") == "wire-b" - ][0] - self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) + missing_summary = summary["batch_missing_terminal_summary"] + self.assertEqual({"device_not_in_3d_scene": 1}, missing_summary["reason_code_counts"]) + self.assertEqual({"该 2D 设备未在 FreeCAD 场景中找到": 1}, missing_summary["reason_label_counts"]) + self.assertIn("该 2D 设备未在 FreeCAD 场景中找到 1 处", message) + self.assertNotIn("重新生成布线连接,刷新缺端子原因诊断", summary["recommended_actions"]) - def test_route_eplan_connections_auto_lane_axis_offsets_perpendicular_to_shared_segment(self): + def test_collect_routing_diagnostic_summary_backfills_issue_codes_from_legacy_missing_batch(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], - project_uuid="project-1", - kind="WireDuct", - ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", + 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.QetDiagnosticOk = False + diagnostic.QetDiagnosticIssueCodes = "" + diagnostic.QetDiagnosticJson = json.dumps( + { + "skipped_missing_terminal": 1, + "route_status_counts": { + "Error": 2, + "MissingTerminal": 1, }, - ], - } - - report = auto_routing.route_eplan_connections_from_payload( - doc, - payload, - options={"lane_spacing": 10.0}, + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "wire_label": "N1", + "start_found": False, + "start_terminal_uuid": "terminal-a", + "start_element_uuid": "device-a", + "start_terminal_display": "A1", + "end_found": True, + } + ], + }, + ensure_ascii=False, ) + diagnostic_group.addObject(diagnostic) - self.assertEqual(1, report["routes"][1]["lane"]["index"]) - self.assertEqual("x", report["routes"][1]["lane"]["axis"]) - routed_group = doc.getObject("QETWiring_04_Routed") - second_wire = [ - wire - for wire in list(getattr(routed_group, "Group", []) or []) - if getattr(wire, "QetWireUuid", "") == "wire-b" - ][0] - self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) - self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) - def test_route_eplan_connections_prefers_unused_alternate_route_segments(self): + self.assertIn("missing_terminals", summary["issue_codes"]) + self.assertIn("missing_endpoints", summary["issue_codes"]) + self.assertIn("routing_errors", summary["issue_codes"]) + self.assertIn("端子匹配失败", summary["issue_labels"]) + self.assertIn("缺失端点", summary["issue_labels"]) + self.assertIn("布线计算错误", summary["issue_labels"]) + self.assertEqual({"Error": 2, "MissingTerminal": 1}, summary["batch_route_status_counts"]) + self.assertIn("结果状态:错误 2 条,缺失端子 1 条", message) + + def test_collect_routing_diagnostic_summary_recommends_device_binding_when_3d_device_missing(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "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( - doc, - [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], - label="Right Bridge", - project_uuid="project-1", - kind="WireDuct", + 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.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": ["missing_terminals"], + "skipped_missing_terminal": 4, + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "wire_label": "F6", + "start_found": False, + "start_terminal_uuid": "device-missing:terminal-a", + "start_element_uuid": "device-missing", + "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, ) - 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", - }, - ], - } + diagnostic_group.addObject(diagnostic) - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) - first_labels = [ - segment["carrier"]["label"] - for segment in report["routes"][0]["route_track"]["segments"] - ] - second_labels = [ - segment["carrier"]["label"] - for segment in report["routes"][1]["route_track"]["segments"] - ] - self.assertIn("Direct Duct", first_labels) - self.assertIn("Alternate Duct", second_labels) - self.assertNotIn("Direct Duct", second_labels) + self.assertIn( + "检查缺失 3D 设备是否已导入、装配并完成 2D/3D 绑定", + summary["recommended_actions"], + ) + self.assertIn("该 2D 设备未在 FreeCAD 场景中找到 1 处", message) + self.assertIn("建议:检查缺失 3D 设备是否已导入", message) - def test_route_eplan_connections_respects_route_segment_capacity_before_detouring(self): + def test_collect_routing_diagnostic_summary_recommends_qet_endpoint_binding_metadata(self): _install_fake_freecad() - terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() - app = sys.modules["FreeCAD"] + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") - _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) - _terminal(doc, terminal_objects, "TerminalStartC", "terminal-start-c", app.Vector(0, 0, 0)) - _terminal(doc, terminal_objects, "TerminalEndC", "terminal-end-c", app.Vector(100, 0, 0)) - direct = routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], - label="Direct Duct", - project_uuid="project-1", - kind="WireDuct", - ) - direct.QetRouteCarrierCapacity = 2 - routing_network.create_route_carrier( - doc, - [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], - label="Left Bridge", - project_uuid="project-1", - kind="WireDuct", - ) - routing_network.create_route_carrier( - doc, - [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], - label="Alternate Duct", - project_uuid="project-1", - kind="WireDuct", + 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.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": ["missing_terminals"], + "skipped_missing_terminal": 1, + "missing_endpoint_samples": [ + { + "wire_uuid": "wire-a", + "start_found": False, + "start_terminal_uuid": "terminal-a", + "start_terminal_display": "A1", + "start_missing_endpoint_reason_code": "missing_device_binding_metadata", + "start_missing_endpoint_reason_label": "导线端点缺少 2D/3D 设备绑定信息", + "end_found": True, + } + ], + }, + ensure_ascii=False, ) - routing_network.create_route_carrier( - doc, - [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], - label="Right Bridge", - project_uuid="project-1", - kind="WireDuct", + diagnostic_group.addObject(diagnostic) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertIn( + "检查 QET 导线端点是否提供 element_uuid 和 terminal_uuid(第一版不要求 start/end_instance_id)", + summary["recommended_actions"], ) - payload = { - "project_uuid": "project-1", - "wires": [ - { - "wire_id": "wire-a", - "start_terminal_uuid": "terminal-start-a", - "end_terminal_uuid": "terminal-end-a", - }, - { - "wire_id": "wire-b", - "start_terminal_uuid": "terminal-start-b", - "end_terminal_uuid": "terminal-end-b", - }, - { - "wire_id": "wire-c", - "start_terminal_uuid": "terminal-start-c", - "end_terminal_uuid": "terminal-end-c", - }, - ], - } + self.assertIn("导线端点缺少 2D/3D 设备绑定信息 1 处", message) - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + def test_collect_routing_diagnostic_summary_recommends_manual_followup_actions(self): + _install_fake_freecad() + terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() + doc = FakeDocument() + 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" + diagnostic.QetProjectUuid = "project-1" + diagnostic.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + { + "issue_codes": ["missing_terminals", "collision_warnings"], + "skipped_missing_terminal": 1, + "collision_warnings": 1, + "top_collision_obstacles": [ + {"label": "NAUO141", "count": 3, "parent_names": ["DoorAssembly"]} + ], + }, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + routed_group = wiring_objects.ensure_routed_group(doc, "project-1") + long_wire = doc.addObject("Part::Feature", "QETRoutedConnection_long") + long_wire.Label = "N-LONG: A1 -> B1" + long_wire.RouteType = "RoutedConnection" + long_wire.QetWireUuid = "wire-long" + long_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] + long_wire.QetStartTerminalUuid = "terminal-a" + long_wire.QetEndTerminalUuid = "terminal-b" + long_wire.QetRouteIssueCodes = "long_terminal_access" + routed_group.addObject(long_wire) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) - route_labels = [ - [segment["carrier"]["label"] for segment in route["route_track"]["segments"]] - for route in report["routes"] - ] - self.assertIn("Direct Duct", route_labels[0]) - self.assertIn("Direct Duct", route_labels[1]) - self.assertIn("Alternate Duct", route_labels[2]) - self.assertNotIn("Direct Duct", route_labels[2]) + self.assertEqual( + [ + "点击“选择缺端子设备”定位需要补工程端子的设备", + "点击“选择异常导线”定位带问题码的导线", + "点击“选择长接入端子/设备”检查设备高度和局部出线路径", + "点击“选择碰撞父装配”确认结构件后再标记忽略碰撞", + ], + summary["recommended_actions"], + ) + self.assertIn("建议:点击“选择缺端子设备”", message) + self.assertIn("点击“选择碰撞父装配”", message) - def test_route_eplan_connections_prefers_unused_segments_occupied_by_existing_wires(self): + def test_collect_routing_diagnostic_summary_recommends_main_path_detour_missing_wires(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)) - 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", + 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.QetDiagnosticOk = False + diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" + diagnostic.QetDiagnosticJson = json.dumps( + {"issue_codes": ["collision_warnings", "main_path_detour_missing"]}, + ensure_ascii=False, + ) + diagnostic_group.addObject(diagnostic) + routed_group = wiring_objects.ensure_routed_group(doc, "project-1") + wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path") + wire.Label = "N-MAINPATH: A1 -> B1" + wire.RouteType = "RoutedConnection" + wire.QetWireUuid = "wire-main-path" + wire.PropertiesList = [ + "QetStartTerminalUuid", + "QetEndTerminalUuid", + "QetRouteDiagnosticsJson", + ] + wire.QetStartTerminalUuid = "terminal-a" + wire.QetEndTerminalUuid = "terminal-b" + wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" + wire.QetRouteDiagnosticsJson = json.dumps( + { + "selective_collision_reroute": { + "status": "RejectedFallback", + "rejected_fallback_kinds": ["RoutingRange"], + "rejected_fallback_labels": ["安装板布线面"], + } + }, + ensure_ascii=False, ) - 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", + routed_group.addObject(wire) + + summary = auto_routing.collect_routing_diagnostic_summary(doc) + message = auto_routing.format_routing_diagnostic_summary(summary) + + self.assertIn( + "点击“选择缺主路径导线”定位需要补 UserPath 或主路径桥接的导线", + summary["recommended_actions"], ) - 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", + self.assertIn( + "选中缺主路径导线后点击“选择拒绝兜底路径”查看需补路径位置", + summary["recommended_actions"], ) - routing_network.create_route_carrier( - doc, - [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], - label="Right Bridge", - project_uuid="project-1", - kind="WireDuct", + self.assertIn( + "点击“选择缺主路径补路位置”快速定位汇总需补区域", + summary["recommended_actions"], ) - auto_routing.route_eplan_connection_between_terminals( - doc, - start, - end, - wire_uuid="existing-wire", + self.assertIn( + "点击“选择缺主路径线路径”对照当前实际路径", + summary["recommended_actions"], ) + self.assertIn("点击“选择缺主路径导线”", message) + self.assertIn("选择缺主路径补路位置", message) + self.assertIn("选择缺主路径线路径", message) + self.assertIn("选择拒绝兜底路径", message) + + def test_routing_preflight_reports_no_routing_sources_when_network_is_empty(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": "new-wire", + "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - route_labels = [ - segment["carrier"]["label"] - for segment in report["routes"][0]["route_track"]["segments"] - ] - self.assertIn("Alternate Duct", route_labels) - self.assertNotIn("Direct Duct", route_labels) + self.assertIn("no_routing_sources", report["issue_codes"]) + self.assertEqual(0, report["routing_sources"]["candidate_sources"]) + self.assertIn("布线源:未识别到线槽/布线面/用户路径", message) + self.assertEqual(0, report["routing_boundaries"]["count"]) + self.assertIn("柜内边界:未标记", message) - def test_route_eplan_connections_report_includes_collision_samples(self): + def test_routing_preflight_reports_cabinet_boundary_count(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] @@ -4859,235 +14768,691 @@ class AutoRoutingTest(unittest.TestCase): 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, 100), app.Vector(100, 0, 100)], - project_uuid="project-1", + boundary = doc.addObject("Part::Feature", "CabinetBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 500)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] ) - obstacle = doc.addObject("Part::Feature", "MiddleObstacle") - obstacle.Label = "Middle Obstacle" - obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", - "wire_label": "N4111", "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) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertEqual(1, report["collision_warnings"]) - self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) - self.assertEqual("N4111", report["collision_samples"][0]["wire_label"]) - self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) - self.assertEqual("HardIntersection", report["collision_samples"][0]["collision_kind"]) - self.assertEqual({"x": 0.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_start"]) - self.assertEqual({"x": 100.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_end"]) - self.assertEqual(40.0, report["collision_samples"][0]["obstacle_bbox"]["xmin"]) - self.assertEqual(35.0, report["collision_samples"][0]["collision_bbox"]["xmin"]) - self.assertEqual("Middle Obstacle", report["routes"][0]["collision_samples"][0]["obstacle_label"]) - self.assertIn("碰撞示例", message) - self.assertIn("Middle Obstacle", message) + self.assertEqual(1, report["routing_boundaries"]["count"]) + self.assertEqual("柜内空间", report["routing_boundaries"]["samples"][0]["label"]) + self.assertIn("柜内边界:1 个", message) - def test_route_eplan_connections_report_calls_out_local_unbound_terminals(self): + def test_routing_preflight_reports_path_network_boundary_issues(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( + _terminal(doc, terminal_objects, "TerminalInside", "terminal-inside", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) + boundary = doc.addObject("Part::Feature", "CabinetBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(-20, 120, -20, 20, -10, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] + ) + routing_network.create_route_carrier( doc, - terminal_objects, - "LocalTerminal", - "local:instance-1:p1", - app.Vector(0, 0, 0), + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="柜内主路径A", + project_uuid="project-1", + kind="UserPath", ) payload = { + "project_uuid": "project-1", "wires": [ { - "wire_id": "wire-1", - "start_terminal_uuid": "qet-terminal-start", - "end_terminal_uuid": "qet-terminal-end", + "wire_id": "wire-outside-terminal", + "wire_label": "N-OUT-TERM", + "start_terminal_uuid": "terminal-inside", + "end_terminal_uuid": "terminal-outside", } - ] + ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertEqual(0, report["routed"]) - self.assertEqual(1, report["available_terminals"]) - self.assertEqual(1, report["local_terminals"]) - self.assertIn("端子匹配失败", message) - self.assertIn("local:", message) + self.assertFalse(report["ok"]) + self.assertIn("terminals_outside_boundary", report["issue_codes"]) + self.assertIn( + "terminals_outside_boundary", + report["routing_path_network_diagnostic"]["issue_codes"], + ) + self.assertEqual( + "terminal-outside", + report["routing_path_network_diagnostic"]["terminals_outside_boundary"][0]["terminal_uuid"], + ) + self.assertIn("路径网络检查提示", message) + self.assertIn("端子越出柜内边界", message) + self.assertIn("越界端子:TerminalOutside(terminal-outside) 2 个越界点", message) - def test_route_eplan_connections_report_includes_network_and_first_error(self): + def test_check_routing_path_network_warns_when_route_carrier_leaves_cabinet_boundary(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "total_wires": 2, - "routed": 1, - "collision_warnings": 1, - "skipped_missing_terminal": 1, - "prepared_layout": { - "wire_duct_carriers": 2, - "surface_carriers": 4, - "terminal_access_carriers": 6, - }, - "missing_endpoint_samples": [ - { - "start_terminal_uuid": "terminal-a", - "end_terminal_uuid": "terminal-b", - } - ], - "errors": ["没有可用的线槽/路由路径网络"], - } + 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)) + boundary = doc.addObject("Part::Feature", "CabinetBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(0, 120, -20, 20, 0, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] + ) + route = routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20), app.Vector(140, 0, 20)], + label="柜内主路径A", + project_uuid="project-1", + kind="UserPath", + ) - message = auto_routing.format_eplan_connection_route_report(report) + 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("routed=1", message) - self.assertIn("线槽路径 2 条", message) - self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) - self.assertIn("缺失示例:terminal-a -> terminal-b", message) + self.assertFalse(result["ok"]) + self.assertIn("route_carriers_outside_boundary", result["issue_codes"]) + self.assertEqual(1, len(payload["route_carriers_outside_boundary"])) + self.assertEqual(route.Name, payload["route_carriers_outside_boundary"][0]["carrier"]["name"]) + self.assertEqual(1, payload["route_carriers_outside_boundary"][0]["outside_point_count"]) + self.assertIn("路径越出柜内边界", message) + self.assertIn("柜内主路径A", message) + self.assertEqual((1.0, 0.0, 0.0), route.ViewObject.LineColor) - def test_route_eplan_connections_report_calls_out_missing_route_network(self): + def test_check_routing_path_network_warns_when_terminal_leaves_cabinet_boundary(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "total_wires": 3, - "routed": 0, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "skipped_missing_route_network": 3, - "route_status_counts": { - "MissingRouteNetwork": 3, - }, - } + 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") + inside = _terminal(doc, terminal_objects, "TerminalInside", "terminal-inside", app.Vector(0, 0, 0)) + outside = _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) + boundary = doc.addObject("Part::Feature", "CabinetBoundary") + boundary.Label = "柜内空间" + boundary.Shape = FakeShape(FakeBoundBox(-20, 120, -20, 20, -10, 80)) + routing_network.mark_cabinet_interior_boundaries_from_selection( + [FakeSelectionItem(obj=boundary)] + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + label="柜内主路径A", + project_uuid="project-1", + kind="UserPath", + ) - message = auto_routing.format_eplan_connection_route_report(report) + 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("缺少布线路径网络 3 条", message) - self.assertIn("请先生成线槽、布线面或布线路径网络", message) + self.assertFalse(result["ok"]) + self.assertIn("terminals_outside_boundary", result["issue_codes"]) + self.assertEqual(1, len(payload["terminals_outside_boundary"])) + self.assertEqual("terminal-outside", payload["terminals_outside_boundary"][0]["terminal_uuid"]) + self.assertIn("端子越出柜内边界", message) + self.assertIn("terminal-outside", message) + self.assertEqual((1.0, 0.0, 0.0), outside.ViewObject.LineColor) + self.assertNotEqual((1.0, 0.0, 0.0), inside.ViewObject.LineColor) - def test_route_eplan_connections_report_includes_missing_route_network_sample(self): + def test_routing_preflight_reports_detected_sources_not_generated(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "total_wires": 1, - "routed": 0, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "skipped_missing_route_network": 1, - "missing_route_network_samples": [ + 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", "MountingPlate") + panel.Label = "安装板" + panel.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 5)) + payload = { + "project_uuid": "project-1", + "wires": [ { - "wire_uuid": "wire-1", - "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", - "error": "没有可用的布线路径网络:起点和终点无法连通", + "end_terminal_uuid": "terminal-end", } ], } - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertIn("缺路径网络示例:导线 N4111", message) - self.assertIn("QF1/A1 (terminal-start) -> KM1/13 (terminal-end)", message) - self.assertIn("原因:没有可用的布线路径网络:起点和终点无法连通", message) + self.assertIn("routing_sources_not_generated", report["issue_codes"]) + self.assertEqual(1, report["routing_sources"]["support_surface_sources"]) + self.assertEqual(1, report["routing_sources"]["candidate_sources"]) + self.assertIn("布线源:线槽 0 个,布线面 1 个", message) + self.assertIn("请先生成布线路径网络", message) - def test_route_eplan_connections_report_includes_readable_missing_endpoint_labels(self): + def test_routing_preflight_reports_unrouteable_wire_sample(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 0, - "collision_warnings": 0, - "skipped_missing_terminal": 1, - "missing_endpoint_samples": [ + 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(5000, 0, 20), app.Vector(5100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ { - "start_terminal_uuid": "terminal-a", - "start_element_uuid": "device-a", - "start_terminal_display": "A1", - "end_terminal_uuid": "terminal-b", - "end_element_uuid": "device-b", - "end_terminal_display": "B1", + "wire_id": "wire-far", + "wire_label": "N-FAR", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", } ], } - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections( + doc, + payload, + options={ + "terminal_access_max_distance": 50.0, + "preflight_routeability_sample_limit": 1, + }, + ) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertIn("缺失示例:device-a/A1 (terminal-a) -> device-b/B1 (terminal-b)", message) + self.assertFalse(report["ok"]) + self.assertEqual(1, report["routeability_checked"]) + self.assertEqual(1, report["unrouteable_wires"]) + self.assertEqual("wire-far", report["unrouteable_samples"][0]["wire_uuid"]) + self.assertIn("unrouteable_wires", report["issue_codes"]) + self.assertIn("导线不可达", message) - def test_route_eplan_connections_report_identifies_which_endpoint_is_missing(self): + def test_routing_preflight_checks_routeability_for_complete_wires_when_other_wires_miss_endpoints(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 0, - "collision_warnings": 0, - "skipped_missing_terminal": 1, - "missing_endpoint_samples": [ + 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(5000, 0, 20), app.Vector(5100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + payload = { + "project_uuid": "project-1", + "wires": [ { + "wire_id": "wire-missing", + "wire_label": "N-MISS", "start_terminal_uuid": "terminal-start", - "start_found": True, "end_terminal_uuid": "terminal-missing", - "end_found": False, - } + }, + { + "wire_id": "wire-far", + "wire_label": "N-FAR", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, ], } - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections( + doc, + payload, + options={ + "terminal_access_max_distance": 50.0, + "preflight_routeability_sample_limit": 1, + }, + ) - self.assertIn("缺失示例:terminal-start -> terminal-missing", message) - self.assertIn("缺失:终点", message) + self.assertFalse(report["ok"]) + self.assertEqual(["terminal-missing"], report["missing_endpoint_uuids"]) + self.assertEqual(1, report["routeability_checked"]) + self.assertEqual(1, report["unrouteable_wires"]) + self.assertIn("missing_endpoints", report["issue_codes"]) + self.assertIn("unrouteable_wires", report["issue_codes"]) - def test_route_eplan_connections_report_calls_out_clearance_collision_kind(self): + def test_routing_preflight_disables_routeability_sampling_by_default(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 1, - "collision_warnings": 1, - "skipped_missing_terminal": 0, - "collision_samples": [ + 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_label": "N4111", - "obstacle_label": "柜体侧板", - "collision_kind": "ClearanceWarning", - } + "wire_id": "wire-a", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, ], } - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertIn("安全间隙", message) - self.assertIn("柜体侧板", message) + self.assertEqual(0, report["routeability_sample_limit"]) + self.assertEqual(0, report["routeability_checked"]) + self.assertNotIn("可达性抽样", message) - def test_route_eplan_connections_report_ignores_non_numeric_status_counts(self): + def test_routing_preflight_report_shows_routeability_sample_coverage(self): _install_fake_freecad() - _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() - report = { - "routed": 1, - "collision_warnings": 0, - "skipped_missing_terminal": 0, - "route_status_counts": { - "Routed": "1", - "ExternalStatus": "not-a-number", - }, + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) + _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 10, 0)) + _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 10, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 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-a", + "end_terminal_uuid": "terminal-end-a", + }, + { + "wire_id": "wire-b", + "start_terminal_uuid": "terminal-start-b", + "end_terminal_uuid": "terminal-end-b", + }, + ], } - message = auto_routing.format_eplan_connection_route_report(report) + report = auto_routing.preflight_eplan_connections( + doc, + payload, + options={"preflight_routeability_sample_limit": 1}, + ) + message = auto_routing.format_eplan_routing_preflight_report(report) - self.assertIn("正常 1 条", message) - self.assertNotIn("ExternalStatus", message) + self.assertEqual(1, report["routeability_checked"]) + self.assertEqual(1, report["routeability_unchecked_wires"]) + self.assertIn("可达性抽样:已检查 1 条,未检查 1 条", message) + + def test_routing_preflight_checks_wire_style_ids_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="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.db" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT, + name TEXT, + line_color TEXT + ) + """ + ) + connection.execute( + "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", + (7, "project-1", "绿色控制线", "#00ff00"), + ) + connection.commit() + finally: + connection.close() + payload = { + "project_uuid": "project-1", + "wire_style_database_path": str(db_path), + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "7", + }, + { + "wire_id": "wire-2", + "wire_label": "N2", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "404", + }, + { + "wire_id": "wire-3", + "wire_label": "N3", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + }, + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + + self.assertFalse(report["ok"]) + self.assertEqual("Available", report["wire_style_database"]["status"]) + self.assertEqual(1, report["wire_style"]["resolved"]) + self.assertEqual(1, report["wire_style"]["missing"]) + self.assertEqual(1, report["wire_style"]["without_style_id"]) + self.assertEqual("404", report["wire_style"]["missing_samples"][0]["wire_style_id"]) + self.assertIn("missing_wire_styles", report["issue_codes"]) + self.assertIn("wires_without_style_id", report["issue_codes"]) + self.assertIn("导线样式库:可用", message) + self.assertIn("已解析 1 条", message) + self.assertIn("缺失样式 1 条", message) + self.assertIn("未设置样式 1 条", message) + + def test_routing_preflight_reports_empty_wire_style_database(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + db_path = Path(temp_dir) / "project-local.db" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT, + name TEXT, + line_color TEXT + ) + """ + ) + connection.commit() + finally: + connection.close() + payload = { + "project_uuid": "project-1", + "wire_style_database_path": str(db_path), + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "1", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + + self.assertFalse(report["ok"]) + self.assertEqual("EmptyWirePropertiesTable", report["wire_style_database"]["status"]) + self.assertEqual(0, report["wire_style_database"]["wire_properties_count"]) + self.assertIn("wire_style_database_empty", report["issue_codes"]) + self.assertIn("导线样式库:wire_properties 为空", message) + + def test_routing_preflight_uses_matching_fallback_style_database_when_payload_database_is_empty(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + wrong_dir = Path(temp_dir) / "wrong" / "datafiles" + right_dir = Path(temp_dir) / "right" / "datafiles" + exchange_dir = Path(temp_dir) / "right" / ".qet_freecad" + wrong_dir.mkdir(parents=True) + right_dir.mkdir(parents=True) + exchange_dir.mkdir(parents=True) + wrong_db = wrong_dir / "project-local.db" + right_db = right_dir / "project-local.db" + for db_path, rows in ( + (wrong_db, []), + (right_db, [(1, "project-1", "红色动力线", "#ff0000")]), + ): + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT, + name TEXT, + line_color TEXT + ) + """ + ) + connection.executemany( + "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", + rows, + ) + connection.commit() + finally: + connection.close() + json_path = exchange_dir / "2d_to_3d.json" + json_path.write_text( + json.dumps({"project_uuid": "project-1", "wires": []}), + encoding="utf-8", + ) + app._qet_exchange_summary = {"json_path": str(json_path)} + payload = { + "project_uuid": "project-1", + "wire_style_database_path": str(wrong_db), + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "1", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + + self.assertTrue(report["ok"]) + self.assertEqual(str(right_db), report["wire_style_database"]["path"]) + self.assertEqual(str(wrong_db), report["wire_style_database_fallback_from"]) + self.assertEqual(1, report["wire_style"]["resolved"]) + self.assertNotIn("wire_style_database_empty", report["issue_codes"]) + self.assertIn("从备用库", message) + + def test_routing_preflight_does_not_backfill_styles_from_other_project_context(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-current") + _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-current", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + json_path = Path(temp_dir) / "2d_to_3d.json" + json_path.write_text( + json.dumps( + { + "project_uuid": "project-old", + "wires": [ + { + "wire_id": "wire-old", + "wire_label": "OLD", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "99", + } + ], + } + ), + encoding="utf-8", + ) + app._qet_exchange_summary = {"json_path": str(json_path)} + payload = { + "project_uuid": "project-current", + "wires": [ + { + "wire_id": "wire-current", + "wire_label": "CURRENT", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + message = auto_routing.format_eplan_routing_preflight_report(report) + + self.assertEqual("project-current", report["project_uuid"]) + self.assertEqual(1, report["total_wires"]) + self.assertEqual(0, report["wire_style"]["with_style_id"]) + self.assertNotIn("OLD", message) + self.assertNotIn("99", message) + + def test_routing_preflight_discovers_style_database_from_exchange_summary_json_path(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + terminal_objects.ensure_root_group(doc, "project-1") + _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) + _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + with tempfile.TemporaryDirectory() as temp_dir: + project_dir = Path(temp_dir) / "project-a" + exchange_dir = project_dir / ".qet_freecad" + data_dir = project_dir / "datafiles" + exchange_dir.mkdir(parents=True) + data_dir.mkdir(parents=True) + json_path = exchange_dir / "2d_to_3d.json" + json_path.write_text( + json.dumps( + { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + "wire_style_id": "1", + } + ], + } + ), + encoding="utf-8", + ) + db_path = data_dir / "project-local.db" + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT, + name TEXT, + line_color TEXT + ) + """ + ) + connection.execute( + "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", + (1, "project-1", "红色动力线", "#ff0000"), + ) + connection.commit() + finally: + connection.close() + app._qet_exchange_summary = {"json_path": str(json_path)} + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N1", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.preflight_eplan_connections(doc, payload) + + self.assertTrue(report["ok"]) + self.assertEqual(str(db_path), report["wire_style_database"]["path"]) + self.assertEqual("Available", report["wire_style_database"]["status"]) + self.assertEqual(1, report["wire_style"]["resolved"]) def test_bind_wire_task_terminals_from_payload_does_not_create_wires(self): _install_fake_freecad() diff --git a/tests/python/freecad_exchange_bootstrap_wiring_test.py b/tests/python/freecad_exchange_bootstrap_wiring_test.py index 570d5a8..1bdad7f 100644 --- a/tests/python/freecad_exchange_bootstrap_wiring_test.py +++ b/tests/python/freecad_exchange_bootstrap_wiring_test.py @@ -1,5 +1,6 @@ import importlib import json +import sqlite3 import sys import tempfile import types @@ -132,6 +133,132 @@ class ExchangeBootstrapWiringTest(unittest.TestCase): self.assertEqual("wire-1", normalized["wires"][0]["wire_id"]) self.assertEqual("W001", normalized["wires"][0]["wire_mark"]) + 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", + "project_uuid": "project-1", + "devices": [], + "terminals": [], + "device_models": [], + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "wire_style_id": 1, + "start_terminal_uuid": "terminal-a", + "end_terminal_uuid": "terminal-b", + } + ], + } + + 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("N4111", normalized["wires"][0]["wire_label"]) + self.assertEqual("1", normalized["wires"][0]["wire_style_id"]) + + 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": "1.2", + "project_uuid": "project-1", + "devices": [], + "terminals": [], + "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" + path.write_text(json.dumps(payload), encoding="utf-8") + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + line_color TEXT + ) + """ + ) + connection.commit() + finally: + connection.close() + + normalized = bootstrap.load_exchange_payload(str(path)) + + self.assertEqual(str(db_path), normalized["wire_style_database_path"]) + + def test_load_exchange_payload_detects_project_datafiles_wire_properties_database(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "schema_version": "1.2", + "project_uuid": "project-1", + "devices": [], + "terminals": [], + "device_models": [], + "wires": [{"wire_id": "wire-1", "wire_style_id": "1"}], + } + + with tempfile.TemporaryDirectory() as temp_dir: + project_dir = Path(temp_dir) / "project-a" + exchange_dir = project_dir / ".qet_freecad" + data_dir = project_dir / "datafiles" + exchange_dir.mkdir(parents=True) + data_dir.mkdir(parents=True) + path = exchange_dir / "2d_to_3d.json" + db_path = data_dir / "project-local.db" + path.write_text(json.dumps(payload), encoding="utf-8") + connection = sqlite3.connect(str(db_path)) + try: + connection.execute( + """ + CREATE TABLE wire_properties ( + id INTEGER PRIMARY KEY, + project_uuid TEXT NOT NULL, + line_color TEXT + ) + """ + ) + connection.commit() + finally: + connection.close() + + normalized = bootstrap.load_exchange_payload(str(path)) + + self.assertEqual(str(db_path), normalized["wire_style_database_path"]) + + def test_exchange_summary_includes_wire_style_database_path(self): + _install_fake_modules() + sys.modules.pop("ExchangeBootstrap", None) + bootstrap = importlib.import_module("ExchangeBootstrap") + payload = { + "project_uuid": "project-1", + "devices": [], + "terminals": [], + "device_models": [], + "wires": [], + "wire_style_database_path": "D:/project/project-local.sqlite", + } + + summary = bootstrap._build_summary(payload, "D:/project/2d_to_3d.json") + + self.assertEqual( + "D:/project/project-local.sqlite", + summary["wire_style_database_path"], + ) + if __name__ == "__main__": unittest.main() From d1c7169cad1e09b28bac4538534d011bca4e4b98 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Fri, 12 Jun 2026 09:09:50 +0800 Subject: [PATCH 63/63] =?UTF-8?q?feat(freecad):=20=E6=94=AF=E6=8C=81SW?= =?UTF-8?q?=E7=B1=BB=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84=E8=8D=89=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FreeCAD 3D自动布线设计方案.md | 34 +- docs/FreeCAD 机柜装配操作文档.md | 88 ++- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 212 +++++++ src/Mod/FreeCADExchange/RoutingNetwork.py | 535 +++++++++++++++++- .../freecad_exchange_auto_routing_test.py | 345 ++++++++++- 5 files changed, 1177 insertions(+), 37 deletions(-) diff --git a/docs/FreeCAD 3D自动布线设计方案.md b/docs/FreeCAD 3D自动布线设计方案.md index 2882b4e..e754d98 100644 --- a/docs/FreeCAD 3D自动布线设计方案.md +++ b/docs/FreeCAD 3D自动布线设计方案.md @@ -1040,6 +1040,35 @@ SW 教程中路径可以由“创建草图”或“转换草图”得到,并 如果临时画的 Draft 线或草图线明显悬空,可同时选中安装板/柜板上的支撑面 Face 和该路径对象,再执行“选中路径作为用户路径”或“生成布线路径网络”。系统会把路径点投影到支撑面附近,而不是直接使用 Draft 当前工作平面坐标。 +#### 6.4.1 SW 类布线路径草图 MVP + +为贴近 SW Electrical 视频中的“选面/离面画路径,再生成 3D 布线”的操作,当前 `3D 布线连接` 面板提供了轻量的布线路径草图入口。它不是完整复刻 SW Sketcher,而是面向电气布线的 `UserPath` 绘制子集: + +1. `草图离面距离 mm`:创建草图时沿选中 Face 法向偏移指定距离。设为 0 时贴近选中面;设为 30、50 等数值时,可在离安装板或柜门一定距离的位置画路径。 +2. `创建布线路径草图`:先选中机柜面板、安装板、门板或线槽上的一个 Face,再点击该按钮。系统会创建黄色 `布线路径草图`,写入 `AttachmentSupport`、`MapMode=FlatFace`、`AttachmentOffset` 和 `QetRouteSketchMode=ManualUserPathSketch`,并尽量进入 Sketcher 编辑。 +3. 用户在 Sketcher 中用普通线段、折线、水平/垂直/重合等约束画路径。Sketcher 的水平/垂直是草图本地平面方向;如果要画全局 Z 向路径,应选竖直 Face 建草图,或使用正交 3D 路径按钮。 +4. 退出草图后,直接点击 `生成布线路径网络` 即可。系统会自动扫描明确标记为布线路径的草图并生成/刷新 `UserPath` carrier,不要求再手动点一次 `选中路径作为用户路径`。 +5. 为避免误把设备或柜体里的普通建模草图当成布线路径,文档级自动扫描只自动纳入明确标记/命名的路径源,例如 `QetRoutingSourceKind=UserPath`、`ManualUserPathSketch`、名称或标签包含 `UserPath`、`Route Path`、`布线路径`、`走线路径`、`用户路径` 等。普通 `Sketch001` 不会自动转换;如果确实要把普通草图作为路径,仍可选中它后点击 `选中路径作为用户路径`。 +6. 明确标记为 `UserPath` 或 `ManualUserPathSketch` 的草图优先按布线路径处理,即使名称里同时包含“安装板”“门板”等支撑面关键词,也不会被误跳过。这样现场可以使用“安装板布线路径草图”这类更自然的命名。 +7. 草图修改后重新生成布线路径网络会刷新同源 `UserPath`。如果把草图里的线段全部删除,系统会同步删除之前由该草图生成的旧 carrier,并清空源草图上的 carrier 名称记录,避免旧路径继续参与自动布线。 + +对于跨平面或竖向过渡,面板还提供两种 3D 点选路径: + +1. `选中点生成3D路径`:按选择顺序把多个 3D 点、顶点、边端点或带 `Points` 的对象串成一条自由空间 `UserPath`。适合现场补一段明确的空间连接,但可能生成斜线。 +2. `选中点生成正交3D路径`:按选择顺序生成只沿 X/Y/Z 单轴变化的折线路径,并标记 `QetRoutePathMode=Orthogonal3D`。它更接近 SW 3D 草图里锁定 X/Y/Z 方向画路径的效果,也更适合减少斜穿设备或柜体。 + +推荐手测顺序: + +```text +1. 选中安装板/柜门/线槽 Face。 +2. 设置“草图离面距离 mm”。 +3. 点击“创建布线路径草图”。 +4. 在 Sketcher 里画黄色主路径,退出草图。 +5. 如有跨面路径,按顺序选择端点/顶点,点击“选中点生成正交3D路径”。 +6. 点击“生成布线路径网络”。 +7. 点击“检查布线路径网络”和“生成布线连接”。 +``` + `UserPath` 与线槽的关系: ```text @@ -1129,7 +1158,9 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) ## 7. 当前限制 -当前版本可完成布线连接原型,但仍有以下限制: +当前版本已进入第一阶段收尾:可从 QET 导线任务生成 FreeCAD 侧布线路径网络和 `AutoSuggested` 导线;可识别/生成 `WireDuct`、`UserPath`、`WiringCutOut`、`RoutingRange`、`TerminalAccess`;可通过“创建布线路径草图”“选中点生成正交 3D 路径”等入口完成接近 SW 的电气布线路径草图/UserPath 绘制 MVP。这个 MVP 已覆盖选 Face 创建离面草图、Sketcher 画线、生成网络自动转 `UserPath`、同源刷新、清空草图后删除旧 carrier、普通机械草图不误转换、真实 Sketcher 坐标不重复应用 Placement 等关键手测场景。 + +但第一阶段仍不是 SW/EPLAN 完整布线系统,主要限制如下: 1. 线槽实体中心线生成基于包围盒长轴,不理解真实线槽开口、盖板和内部空间。 2. 多根线共路时已做基础错位显示,但不是线束级排布,也不计算每根线在线槽内的真实截面位置。 @@ -1141,6 +1172,7 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points) 8. 端子出线方向默认依赖端子 LCS 方向;若工程端子提供 `QetTerminalLocalRoutePointsJson`,会优先使用局部路径。模板端子方向或局部路径不准时,布线连接会受影响。 9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。 10. 当前不自动求解导轨、安装板和设备之间的 Assembly 配合关系;装配位置以 `scene.FCStd` 中对象的最终 `Placement` 为准。 +11. `创建布线路径草图` 只提供面向电气路径的轻量草图入口,不复刻 SW 3D Sketch 的全部约束、捕捉、智能标注和工程规则;跨平面路径当前优先用“选中点生成正交3D路径”补充。 ## 8. 后续需要完成 diff --git a/docs/FreeCAD 机柜装配操作文档.md b/docs/FreeCAD 机柜装配操作文档.md index 9ea9138..cf71a47 100644 --- a/docs/FreeCAD 机柜装配操作文档.md +++ b/docs/FreeCAD 机柜装配操作文档.md @@ -976,15 +976,63 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 1. 面板状态摘要显示 `版本:2026-06-08-runtime-routing-v4` 或更新版本。 2. `检查布线准备度` 能识别到 QET 导线来源、工程端子、路径网络和柜内边界;若有问题,`RoutingPreflight.QetDiagnosticIssueCodes` 能说明原因。 3. `检查布线路径网络` 不应出现空网络、路径对象几何无效、端子越出柜内边界或路径越出柜内边界;如果出现,应先修装配或路径源。 -4. `生成布线连接` 后,`RoutingConnectionBatch.QetDiagnosticJson.runtime_version` 与面板版本一致。 -5. 有导线任务时,`RoutingConnectionBatch.routed` 应大于 0;如果为 0,应优先看 `missing_endpoint_samples`、`missing_route_network_samples` 或 `error_samples`。 -6. 正常导线的单线 `QetRouteIssueCodes` 应为空;若存在问题码,应能归类到端子接入、碰撞、柜内越界、容量压力、路径质量或路径约束。 -7. 已标记 `CabinetInterior` 时,优先要求 `QetRouteBoundaryStatus=InsideBoundary`;出现 `BoundaryWarning` 时,应补柜内主路径或修正边界。 -8. 碰撞状态优先看 `QetRouteCollisionStatus`:`NoCollision` 为理想结果,`ClearanceWarning` 可作为间隙问题记录,`HardIntersectionWarning` 视为穿模问题。 -9. 多根线共路时,检查 `QetRouteLaneIndex`、`QetRouteLaneOffsetMm` 和 `QetRouteCapacityStatus`,确认新增导线不会无诊断地贴到旧线上。 -10. 最后点击 `汇总布线诊断`,把 `RoutingDiagnosticSummary.QetDiagnosticMessage` 和主要 `issue_codes` 作为本次手测结论。 +4. 如果使用 `创建布线路径草图`,退出 Sketcher 后点击 `生成布线路径网络`,应能在树目录看到对应 `UserPath` carrier;源草图应保留 `QetRouteSketchMode=ManualUserPathSketch`,生成的 carrier 应保留 `QetRouteSourceLabel` / `QetRouteSourceName`。 +5. 如果在竖直 Face 上创建草图,生成的 `UserPath` 点应沿该 Face 的离面方向偏移,不应出现离面距离翻倍或路径跑到柜外;若出现,优先记录源草图 Label、离面距离和生成 carrier 的 `Points`。 +6. 如果把布线路径草图里的线段全部删掉,再点击 `生成布线路径网络`,之前由该草图生成的旧 `UserPath` 应被清理,不应继续参与布线。 +7. `生成布线连接` 后,`RoutingConnectionBatch.QetDiagnosticJson.runtime_version` 与面板版本一致。 +8. 有导线任务时,`RoutingConnectionBatch.routed` 应大于 0;如果为 0,应优先看 `missing_endpoint_samples`、`missing_route_network_samples` 或 `error_samples`。 +9. 正常导线的单线 `QetRouteIssueCodes` 应为空;若存在问题码,应能归类到端子接入、碰撞、柜内越界、容量压力、路径质量或路径约束。 +10. 已标记 `CabinetInterior` 时,优先要求 `QetRouteBoundaryStatus=InsideBoundary`;出现 `BoundaryWarning` 时,应补柜内主路径或修正边界。 +11. 碰撞状态优先看 `QetRouteCollisionStatus`:`NoCollision` 为理想结果,`ClearanceWarning` 可作为间隙问题记录,`HardIntersectionWarning` 视为穿模问题。 +12. 多根线共路时,检查 `QetRouteLaneIndex`、`QetRouteLaneOffsetMm` 和 `QetRouteCapacityStatus`,确认新增导线不会无诊断地贴到旧线上。 +13. 最后点击 `汇总布线诊断`,把 `RoutingDiagnosticSummary.QetDiagnosticMessage` 和主要 `issue_codes` 作为本次手测结论。 -如果上面 1、4、5 成立,且问题导线都能通过诊断字段定位原因,说明当前版本已经具备第一版可测闭环。若仍存在导线穿模、柜外线或未布通,应优先把对应导线的 `wire_object_label`、`QetRouteIssueCodes`、`QetRouteDiagnosticsJson` 和录屏时间点一起反馈。 +如果上面 1、4、7、8 成立,且问题导线都能通过诊断字段定位原因,说明当前版本已经具备第一版可测闭环。若仍存在导线穿模、柜外线或未布通,应优先把对应导线的 `wire_object_label`、`QetRouteIssueCodes`、`QetRouteDiagnosticsJson` 和录屏时间点一起反馈。 + +### 14.3 手测反馈记录模板 + +建议每次 GUI 手测后按下面格式记录,便于开发侧快速判断问题来自装配、路径网络、端子接入还是导线求路: + +```text +测试工程: +FreeCAD 版本/运行目录: +面板 runtime_version: +是否从 QET 3D 按钮打开: +是否重新生成工程端子: +是否标记 CabinetInterior: + +路径输入: +- 线槽数量/是否标记为线槽: +- UserPath 来源:创建布线路径草图 / 选中路径作为用户路径 / 选中点生成正交3D路径 / 诊断桥接 +- 是否使用竖直 Face 创建草图: +- 草图离面距离 mm: +- 生成布线路径网络后 UserPath carrier 数量: + +批量布线结果: +- total_wires: +- routed: +- missing_terminals: +- missing_route_network: +- collision_warnings: +- boundary warnings: +- main_path_not_used / fallback_routes: + +汇总诊断: +- RoutingDiagnosticSummary.QetDiagnosticMessage: +- 主要 issue_codes: +- 下一步建议动作: + +异常样例: +- 录屏时间点: +- wire_object_label: +- QetRouteIssueCodes: +- QetRouteSourceLabels / QetRouteCarrierNames: +- QetRouteAccessStatus: +- QetRouteCollisionStatus: +- QetRouteBoundaryStatus: +- QetRouteQualityStatus: +- 简要现象:未布通 / 穿模 / 跑出柜外 / 接入过长 / 未走主路径 / 线样式不对 +``` --- @@ -1040,13 +1088,23 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。 1. 先把导轨、线槽和设备摆到机柜内真实位置。 2. 选择线槽,点击 `标记为线槽`。 -3. 如果没有线槽,先画草图或 Draft 线,再点击 `选中路径作为用户路径`。 -4. 打开 `3D布线连接`。 -5. 点击 `检查布线准备度`,确认有布线源。 -6. 点击 `准备布线布局空间`。 -7. 点击 `生成布线路径网络`。 -8. 点击 `检查布线路径网络`。 -9. 再尝试生成布线连接。 +3. 如果没有线槽,或需要补柜内主路径,优先使用 `3D布线连接` 面板的 SW 类路径流程: + - 选中安装板、柜门、线槽或柜板上的一个 Face。 + - 设置 `草图离面距离 mm`,例如 0、20、50。 + - 点击 `创建布线路径草图`。 + - 在 Sketcher 中画横竖路径线,退出草图。 + - 点击 `生成布线路径网络`,系统会自动把明确标记的布线路径草图转换为 `UserPath`。 + - 如果要画全局 Z 向或竖向过渡路径,优先选柜侧板、门板内侧、线槽侧面等竖直 Face 创建草图;Sketcher 里的水平/垂直约束始终相对当前草图平面。 +4. 如果需要跨平面或 Z 向过渡路径,按顺序选择多个 3D 点、顶点或边端点,再点击 `选中点生成正交3D路径`。该路径会按 X/Y/Z 折线生成,适合补线槽之间、门板到柜内、设备区域到主路径之间的连接。 +5. 如果已经有普通草图或 Draft 线,也可以选中它后点击 `选中路径作为用户路径`。普通 `Sketch001` 这类未标记机械草图不会被 `生成布线路径网络` 自动当成布线路径,避免误转换。 + - 通过 `创建布线路径草图` 生成的草图已经写入布线路径标记,可以命名为“安装板布线路径草图”“门板布线路径草图”等;即使名称里带安装板、门板这类支撑面词,也会按 `UserPath` 处理。 + - 如果回到草图里把路径线全部删掉,再点击 `生成布线路径网络` 会清理之前生成的旧 `UserPath`,不用手动删除树目录里的旧 carrier。 +6. 打开 `3D布线连接`。 +7. 点击 `检查布线准备度`,确认有布线源。 +8. 点击 `准备布线布局空间`。 +9. 点击 `生成布线路径网络`。 +10. 点击 `检查布线路径网络`。 +11. 再尝试生成布线连接。 ### 15.4 为什么保存后 QET 看不到 3D 位姿? diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 566f062..8453951 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -101,6 +101,7 @@ class AutoRoutingController: def __init__(self, options=None): self.last_report = None self.options = dict(options or {}) + self.options.setdefault("user_path_sketch_offset", RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET) def routing_options(self): return dict(self.options) @@ -174,6 +175,13 @@ class AutoRoutingController: capacity = 1 self.options["selected_route_capacity"] = max(capacity, 1) + def set_user_path_sketch_offset(self, value): + try: + offset = float(value) + except Exception: + offset = RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET + self.options["user_path_sketch_offset"] = max(offset, 0.0) + def set_preflight_routeability_sample_limit(self, value): try: sample_limit = int(value) @@ -1846,6 +1854,84 @@ class AutoRoutingController: } return self.last_report + def create_user_path_sketch_from_selection(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + result = RoutingNetwork.create_user_path_sketch_from_selection( + doc, + _selection_ex(), + project_uuid=project_uuid, + offset=float( + self.routing_options().get( + "user_path_sketch_offset", + RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET, + ) + or 0.0 + ), + ) + sketch = result.get("sketch") + self.last_report = { + "user_path_sketches": 1 if sketch is not None else 0, + "user_path_sketch_names": [getattr(sketch, "Name", "")] if sketch is not None else [], + "support_name": getattr(result.get("support"), "Name", "") if result.get("support") is not None else "", + "support_sub_element": result.get("sub_element_name", ""), + "sketch": sketch, + } + return self.last_report + + def create_user_path_from_selected_points(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + result = RoutingNetwork.create_user_path_carrier_from_selected_points( + doc, + _selection_ex(), + project_uuid=project_uuid, + ) + carrier = result.get("carrier") + self.last_report = { + "user_path_carriers": 1 if carrier is not None else 0, + "user_path_carrier_names": [getattr(carrier, "Name", "")] if carrier is not None else [], + "user_path_point_count": len(result.get("points", []) or []), + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + + def create_orthogonal_user_path_from_selected_points(self): + doc = _active_document() + project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip() + result = RoutingNetwork.create_orthogonal_user_path_carrier_from_selected_points( + doc, + _selection_ex(), + project_uuid=project_uuid, + ) + carrier = result.get("carrier") + self.last_report = { + "user_path_carriers": 1 if carrier is not None else 0, + "user_path_carrier_names": [getattr(carrier, "Name", "")] if carrier is not None else [], + "user_path_point_count": len(result.get("points", []) or []), + "user_path_mode": "Orthogonal3D", + "network": RoutingNetwork.network_summary( + doc, + adjoining_duct_tolerance=float( + self.routing_options().get( + "adjoining_duct_tolerance", + RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE, + ) + or 0.0 + ), + ), + } + return self.last_report + def set_selected_terminal_local_route_points(self): result = RoutingNetwork.set_terminal_local_route_points_from_selection(_selection_ex()) terminal = result.get("terminal") @@ -2326,6 +2412,21 @@ class AutoRoutingTaskPanel: int(self.controller.routing_options().get("selected_route_capacity", 4) or 4) ) options_layout.addWidget(self.selected_route_capacity_spin) + options_layout.addWidget(QtWidgets.QLabel("草图离面距离 mm")) + self.user_path_sketch_offset_spin = QtWidgets.QDoubleSpinBox() + self.user_path_sketch_offset_spin.setRange(0.0, 10000.0) + self.user_path_sketch_offset_spin.setDecimals(1) + self.user_path_sketch_offset_spin.setSingleStep(5.0) + self.user_path_sketch_offset_spin.setValue( + float( + self.controller.routing_options().get( + "user_path_sketch_offset", + RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET, + ) + or 0.0 + ) + ) + options_layout.addWidget(self.user_path_sketch_offset_spin) self.generate_layout_button = _style_command_button( QtWidgets.QPushButton(), @@ -2339,6 +2440,24 @@ class AutoRoutingTaskPanel: "按 EPLAN 逻辑从布局空间生成完整 routing path network:线槽、布线区域和端子接入。", ) + self.create_user_path_sketch_button = _style_command_button( + QtWidgets.QPushButton(), + "创建布线路径草图", + "先选中机柜面板、安装板、门板或线槽的一个 Face,再创建一次性贴面草图;生成布线路径网络时会自动转换为 UserPath。", + ) + + self.create_3d_user_path_button = _style_command_button( + QtWidgets.QPushButton(), + "选中点生成3D路径", + "按选择顺序把多个 3D 点、顶点、边端点或带 Points 的对象串成一条 UserPath,适合补竖向或跨平面路径。", + ) + + self.create_orthogonal_3d_user_path_button = _style_command_button( + QtWidgets.QPushButton(), + "选中点生成正交3D路径", + "按选择顺序把多个 3D 点串成只沿 X/Y/Z 单轴变化的折线路径,减少斜穿设备和柜体。", + ) + self.create_user_paths_button = _style_command_button( QtWidgets.QPushButton(), "选中路径作为用户路径", @@ -2558,6 +2677,9 @@ class AutoRoutingTaskPanel: for widget in ( self.generate_layout_button, + self.create_user_path_sketch_button, + self.create_3d_user_path_button, + self.create_orthogonal_3d_user_path_button, self.create_user_paths_button, self.set_terminal_local_route_button, self.create_user_path_bridge_button, @@ -2604,6 +2726,9 @@ class AutoRoutingTaskPanel: layout.addWidget(self.status_label) self.generate_paths_button.clicked.connect(self.generate_routing_paths) + self.create_user_path_sketch_button.clicked.connect(self.create_user_path_sketch_from_selection) + self.create_3d_user_path_button.clicked.connect(self.create_user_path_from_selected_points) + 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.create_user_path_bridge_button.clicked.connect(self.create_user_path_bridge_from_selection) @@ -2686,6 +2811,7 @@ class AutoRoutingTaskPanel: self.controller.set_lane_max_offset(self.lane_max_offset_spin.value()) 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()) def generate_routing_paths(self): try: @@ -2715,6 +2841,92 @@ class AutoRoutingTaskPanel: except Exception as exc: self._set_error(str(exc)) + def _select_and_edit_sketch(self, sketch): + if Gui is None or sketch is None: + return False + try: + Gui.Selection.clearSelection() + except Exception: + pass + try: + Gui.Selection.addSelection(sketch) + except Exception: + try: + doc = getattr(sketch, "Document", None) + Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(sketch, "Name", "")) + except Exception: + pass + try: + active_gui_doc = getattr(Gui, "ActiveDocument", None) + if active_gui_doc is not None: + active_gui_doc.setEdit(getattr(sketch, "Name", "")) + return True + except Exception: + pass + return False + + def create_user_path_sketch_from_selection(self): + try: + self._sync_options_from_widgets() + result = self.controller.create_user_path_sketch_from_selection() + sketch = result.get("sketch") + edited = self._select_and_edit_sketch(sketch) + sketch_name = getattr(sketch, "Label", "") or getattr(sketch, "Name", "") or "布线路径草图" + edit_text = "已进入草图编辑;" if edited else "已选中新草图,可双击进入编辑;" + self._set_status( + "已创建{0}:{1}请在草图里画布线路径线,退出草图后直接点击“生成布线路径网络”即可自动生成/刷新 UserPath;如果要立即转换当前草图,也可以选中它点击“选中路径作为用户路径”。{2}".format( + sketch_name, + edit_text, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def create_user_path_from_selected_points(self): + try: + self._sync_options_from_widgets() + result = self.controller.create_user_path_from_selected_points() + count = result.get("user_path_carriers", 0) + point_count = result.get("user_path_point_count", 0) + if count <= 0: + self._set_status( + "未生成 3D 用户路径。请按顺序选择至少两个 3D 点、顶点或边端点。" + + self.controller.summary() + ) + return + self._set_status( + "已生成 3D 用户路径:{0} 条,路径点 {1} 个。重新生成布线路径网络后会参与自动布线。{2}".format( + count, + point_count, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + + def create_orthogonal_user_path_from_selected_points(self): + try: + self._sync_options_from_widgets() + result = self.controller.create_orthogonal_user_path_from_selected_points() + count = result.get("user_path_carriers", 0) + point_count = result.get("user_path_point_count", 0) + if count <= 0: + self._set_status( + "未生成正交 3D 用户路径。请按顺序选择至少两个 3D 点、顶点或边端点。" + + self.controller.summary() + ) + return + self._set_status( + "已生成正交 3D 用户路径:{0} 条,路径点 {1} 个。路径只沿 X/Y/Z 单轴折线前进,重新生成布线路径网络后会参与自动布线。{2}".format( + count, + point_count, + self.controller.summary(), + ) + ) + except Exception as exc: + self._set_error(str(exc)) + def create_user_paths_from_selection(self): try: self._sync_options_from_widgets() diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index e087838..126ab24 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -57,6 +57,7 @@ DEFAULT_TERMINAL_ACCESS_COMPONENT_SEGMENT_PENALTY = 25.0 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_ADJOINING_DUCT_TOLERANCE = 5.0 DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 WIRE_DUCT_OBSTACLE_MODE = "PassThrough" @@ -700,6 +701,70 @@ def _set_user_path_source_semantics(source): ) +def _set_user_path_sketch_semantics(sketch, project_uuid="", support=None, sub_element_name="", offset=DEFAULT_ROUTE_PATH_FACE_OFFSET): + _set_user_path_source_semantics(sketch) + TerminalObjects.ensure_string_property( + sketch, + "QetProjectUuid", + PROPERTY_GROUP, + "Project UUID for this route sketch", + project_uuid, + ) + TerminalObjects.ensure_string_property( + sketch, + "QetRouteSketchMode", + PROPERTY_GROUP, + "Manual route sketch mode", + "ManualUserPathSketch", + ) + TerminalObjects.ensure_string_property( + sketch, + "QetRouteSketchSupportName", + PROPERTY_GROUP, + "One-shot support object used when creating this manual route sketch", + getattr(support, "Name", "") if support is not None else "", + ) + TerminalObjects.ensure_string_property( + sketch, + "QetRouteSketchSupportLabel", + PROPERTY_GROUP, + "One-shot support label used when creating this manual route sketch", + getattr(support, "Label", "") if support is not None else "", + ) + TerminalObjects.ensure_string_property( + sketch, + "QetRouteSketchSupportSubElement", + PROPERTY_GROUP, + "One-shot support sub-element used when creating this manual route sketch", + sub_element_name or "", + ) + _ensure_float_property( + sketch, + "QetRouteSketchFaceOffsetMm", + "Offset from selected support face when creating this manual route sketch", + offset, + ) + + +def _style_user_path_sketch(sketch): + view = getattr(sketch, "ViewObject", None) + if view is None: + return + for attr_name, value in ( + ("LineColor", (1.0, 0.85, 0.0)), + ("ShapeColor", (1.0, 0.85, 0.0)), + ("PointColor", (1.0, 0.85, 0.0)), + ): + try: + setattr(view, attr_name, value) + except Exception: + pass + try: + view.LineWidth = 3.0 + except Exception: + pass + + def _object_has_bbox(obj): shape = getattr(obj, "Shape", None) return getattr(shape, "BoundBox", None) is not None @@ -1222,6 +1287,14 @@ def _object_global_placement(obj): return getattr(obj, "Placement", None) +def _route_source_geometry_placement(obj): + type_id = (getattr(obj, "TypeId", "") or "").lower() + if "sketch" in type_id: + # 真实 Sketcher::SketchObject 的 Shape 点已经带有 Attachment/Placement,不能再平移一次。 + return None + return _object_global_placement(obj) + + def _points_with_placement(points, placement): return [_placement_mult_vec(placement, _vector(point)) for point in points] @@ -1283,6 +1356,13 @@ def _routing_source_text(obj): ).lower() +def _is_explicit_user_path_source(obj): + return ( + (getattr(obj, "QetRoutingSourceKind", "") or "").strip() == ROUTE_CARRIER_KIND_USER_PATH + or (getattr(obj, "QetRouteSketchMode", "") or "").strip() == "ManualUserPathSketch" + ) + + def _bbox_aspect_ratio(bbox): extents = sorted( (_bbox_extent(bbox, axis) for axis in ("x", "y", "z")), @@ -1300,6 +1380,8 @@ def _is_wire_duct_candidate(obj, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT): return False if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): return False + if _is_explicit_user_path_source(obj): + return False if (getattr(obj, "RouteType", "") or "").strip(): return False text = _routing_source_text(obj) @@ -1345,6 +1427,8 @@ def _is_support_surface_candidate(obj): return False if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): return False + if _is_explicit_user_path_source(obj): + return False if (getattr(obj, "RouteType", "") or "").strip(): return False text = _routing_source_text(obj) @@ -1372,6 +1456,8 @@ def _is_wiring_cut_out_candidate(obj): return False if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): return False + if _is_explicit_user_path_source(obj): + return False if (getattr(obj, "RouteType", "") or "").strip(): return False @@ -1437,6 +1523,7 @@ def _point_runs_from_selection_item(selection_item): points = [] obj = getattr(selection_item, "Object", None) placement = _object_global_placement(obj) + geometry_placement = _route_source_geometry_placement(obj) for point in list(getattr(selection_item, "PickedPoints", []) or []): points.append(_vector(point)) @@ -1447,19 +1534,19 @@ def _point_runs_from_selection_item(selection_item): if points: runs.append(points) points = [] - runs.append(_points_with_placement(_wire_points(sub_object), placement)) + runs.append(_points_with_placement(_wire_points(sub_object), geometry_placement)) continue if shape_type == "edge": - points.extend(_points_with_placement(_edge_points(sub_object), placement)) + points.extend(_points_with_placement(_edge_points(sub_object), geometry_placement)) continue if shape_type == "vertex": point = getattr(sub_object, "Point", None) if point is not None: - points.append(_placement_mult_vec(placement, _vector(point))) + points.append(_placement_mult_vec(geometry_placement, _vector(point))) continue center = _shape_center(sub_object) if center is not None: - points.append(_placement_mult_vec(placement, center)) + points.append(_placement_mult_vec(geometry_placement, center)) if obj is not None and _is_route_path_source_object(obj): for point in list(getattr(obj, "Points", []) or []): @@ -1473,14 +1560,14 @@ def _point_runs_from_selection_item(selection_item): runs.append(points) points = [] for wire in wires: - runs.append(_points_with_placement(_wire_points(wire), placement)) + runs.append(_points_with_placement(_wire_points(wire), geometry_placement)) else: for edge in list(getattr(shape, "Edges", []) or []): - points.extend(_points_with_placement(_edge_points(edge), placement)) + points.extend(_points_with_placement(_edge_points(edge), geometry_placement)) if not runs and not points: center = _shape_center(shape) if center is not None: - points.append(_placement_mult_vec(placement, center)) + points.append(_placement_mult_vec(geometry_placement, center)) if points: runs.append(points) @@ -1508,6 +1595,21 @@ def _support_face_from_selection(selection_ex): return None +def _support_face_selection_from_selection(selection_ex): + for item in selection_ex or []: + source = getattr(item, "Object", None) + sub_names = list(getattr(item, "SubElementNames", []) or []) + for index, sub_object in enumerate(list(getattr(item, "SubObjects", []) or [])): + if (getattr(sub_object, "ShapeType", "") or "").lower() != "face": + continue + return { + "face": sub_object, + "source": source, + "sub_element_name": sub_names[index] if index < len(sub_names) else "", + } + return None + + def _selection_item_is_only_support_face(selection_item): sub_objects = list(getattr(selection_item, "SubObjects", []) or []) if not sub_objects: @@ -1703,6 +1805,216 @@ def _project_points_to_face(points, face, offset=DEFAULT_ROUTE_PATH_FACE_OFFSET) return projected +def _rotation_from_face_normal(normal): + try: + rotation = App.Rotation(App.Vector(0, 0, 1), normal) + if rotation is not None: + return rotation + except Exception: + pass + try: + return App.Rotation() + except Exception: + return None + + +def _attach_sketch_to_support_face(sketch, support, sub_element_name, offset, fallback_base, normal): + if sketch is None: + return + attached = False + if support is not None and sub_element_name: + try: + sketch.AttachmentSupport = [(support, sub_element_name)] + sketch.MapMode = "FlatFace" + sketch.AttachmentOffset = App.Placement( + App.Vector(0, 0, abs(float(offset or 0.0))), + App.Rotation(), + ) + attached = True + except Exception: + attached = False + try: + sketch.Placement = App.Placement(fallback_base, _rotation_from_face_normal(normal)) + except Exception: + pass + if attached: + # Attachment 是真实 FreeCAD 里的编辑语义;Placement 仍保留为测试和旧对象兜底。 + return + + +def create_user_path_sketch_from_selection( + doc, + selection_ex, + project_uuid="", + offset=DEFAULT_ROUTE_PATH_FACE_OFFSET, +): + """Create a one-shot Sketcher sketch on the selected cabinet/support face for manual UserPath drawing.""" + if doc is None: + raise RoutingNetworkError("没有可用的 FreeCAD 文档。") + + support = _support_face_selection_from_selection(selection_ex) + if support is None: + raise RoutingNetworkError("请先选中机柜面板、安装板、门板或线槽上的一个面 Face,再创建布线路径草图。") + + face = support["face"] + normal = _normalize(_face_normal(face)) + if normal is None: + raise RoutingNetworkError("选中的 Face 无法确定法向,不能创建布线路径草图。") + + face_points = _face_points(face) + origin = _face_origin(face, face_points) + base = _add(origin, _scale(normal, abs(float(offset or 0.0)))) + name = _unique_name(doc, "QETUserPathSketch") + sketch = doc.addObject("Sketcher::SketchObject", name) + sketch.Label = "布线路径草图" + _attach_sketch_to_support_face( + sketch, + support.get("source"), + support.get("sub_element_name", ""), + abs(float(offset or 0.0)), + base, + normal, + ) + _set_user_path_sketch_semantics( + sketch, + project_uuid=project_uuid, + support=support.get("source"), + sub_element_name=support.get("sub_element_name", ""), + offset=abs(float(offset or 0.0)), + ) + _style_user_path_sketch(sketch) + try: + doc.recompute() + except Exception: + pass + return { + "sketch": sketch, + "support": support.get("source"), + "sub_element_name": support.get("sub_element_name", ""), + "offset": abs(float(offset or 0.0)), + } + + +def _selected_points_from_selection(selection_ex): + points = [] + for item in selection_ex or []: + obj = getattr(item, "Object", None) + placement = _object_global_placement(obj) + geometry_placement = _route_source_geometry_placement(obj) + for point in list(getattr(item, "PickedPoints", []) or []): + points.append(_vector(point)) + for sub_object in list(getattr(item, "SubObjects", []) or []): + shape_type = (getattr(sub_object, "ShapeType", "") or "").lower() + if shape_type == "wire": + points.extend(_points_with_placement(_wire_points(sub_object), geometry_placement)) + continue + if shape_type == "edge": + points.extend(_points_with_placement(_edge_points(sub_object), geometry_placement)) + continue + if shape_type == "vertex": + point = getattr(sub_object, "Point", None) + if point is not None: + points.append(_placement_mult_vec(geometry_placement, _vector(point))) + continue + center = _shape_center(sub_object) + if center is not None: + points.append(_placement_mult_vec(geometry_placement, center)) + if obj is not None and _is_route_path_source_object(obj): + for point in list(getattr(obj, "Points", []) or []): + points.append(_placement_mult_vec(placement, _vector(point))) + return _normalize_point_run(points) + + +def create_user_path_carrier_from_selected_points(doc, selection_ex, project_uuid="", label="QET 3D User Route Path"): + """Create one 3D UserPath from selected vertices, picked points, or point-like sub-objects in selection order.""" + if doc is None: + raise RoutingNetworkError("没有可用的 FreeCAD 文档。") + points = _selected_points_from_selection(selection_ex) + if len(points) < 2: + raise RoutingNetworkError("请至少按顺序选择两个 3D 点、顶点、边端点或带 Points 的路径对象。") + carrier = create_route_carrier( + doc, + points, + label=label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + ) + try: + doc.recompute() + except Exception: + pass + return { + "carrier": carrier, + "points": points, + } + + +def _orthogonal_points_between(start, end, axis_order=("x", "y", "z")): + current = _vector(start) + end = _vector(end) + points = [current] + for axis in axis_order or ("x", "y", "z"): + if abs(_axis_value(current, axis) - _axis_value(end, axis)) <= DEFAULT_NODE_TOLERANCE: + continue + current = _set_axis(current, axis, _axis_value(end, axis)) + if _distance(points[-1], current) > DEFAULT_NODE_TOLERANCE: + points.append(current) + if _distance(points[-1], end) > DEFAULT_NODE_TOLERANCE: + points.append(end) + return points + + +def _orthogonalize_points(points, axis_order=("x", "y", "z")): + source_points = _normalize_point_run([_vector(point) for point in points or []]) + if len(source_points) < 2: + return source_points + result = [source_points[0]] + for end in source_points[1:]: + segment_points = _orthogonal_points_between(result[-1], end, axis_order=axis_order) + for point in segment_points[1:]: + if _distance(result[-1], point) > DEFAULT_NODE_TOLERANCE: + result.append(point) + return _normalize_point_run(result) + + +def create_orthogonal_user_path_carrier_from_selected_points( + doc, + selection_ex, + project_uuid="", + label="QET Orthogonal 3D User Route Path", + axis_order=("x", "y", "z"), +): + """Create one X/Y/Z orthogonal 3D UserPath from selected points in selection order.""" + if doc is None: + raise RoutingNetworkError("没有可用的 FreeCAD 文档。") + points = _selected_points_from_selection(selection_ex) + if len(points) < 2: + raise RoutingNetworkError("请至少按顺序选择两个 3D 点、顶点、边端点或带 Points 的路径对象。") + orthogonal_points = _orthogonalize_points(points, axis_order=axis_order) + carrier = create_route_carrier( + doc, + orthogonal_points, + label=label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + ) + TerminalObjects.ensure_string_property( + carrier, + "QetRoutePathMode", + PROPERTY_GROUP, + "Manual 3D route path mode", + "Orthogonal3D", + ) + try: + doc.recompute() + except Exception: + pass + return { + "carrier": carrier, + "points": orthogonal_points, + } + + def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUTE_CARRIER_KIND): created = [] support_face = _support_face_from_selection(selection_ex) @@ -1762,6 +2074,11 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") point_runs = [_normalize_point_run(points) for points in point_runs] point_runs = [points for points in point_runs if len(points) >= 2] if not point_runs: + if source is not None: + live_carriers = _live_source_carriers(doc, source) + if live_carriers: + _remove_route_carriers(doc, live_carriers) + _mark_user_path_source_carriers(source, []) continue label = "QET User Route Path {0}".format(index) @@ -1821,6 +2138,70 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="") return created +def _create_or_refresh_user_path_source(doc, source, project_uuid="", label_prefix="QET User Route Path"): + if source is None or is_route_carrier(source) or is_routing_boundary(source): + return [] + point_runs = _point_runs_from_selection_item(type("_Selection", (), {"Object": source, "SubObjects": []})()) + point_runs = [_normalize_point_run(points) for points in point_runs] + point_runs = [points for points in point_runs if len(points) >= 2] + if not point_runs: + live_carriers = _live_source_carriers(doc, source) + if live_carriers: + _remove_route_carriers(doc, live_carriers) + _mark_user_path_source_carriers(source, []) + return [] + + label = "{0} {1}".format( + label_prefix, + getattr(source, "Label", "") or getattr(source, "Name", "") or "", + ).strip() + capacity = _route_carrier_capacity_value(source, default=1) + live_carriers = _live_source_carriers(doc, source) + refreshed = [] + for run_index, points in enumerate(point_runs, start=1): + run_label = label if len(point_runs) == 1 else "{0} {1}".format(label, run_index) + if run_index <= len(live_carriers): + carrier = live_carriers[run_index - 1] + if _update_route_carrier( + carrier, + points, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, + ): + refreshed.append(carrier) + continue + refreshed.append( + create_route_carrier( + doc, + points, + label=run_label, + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_USER_PATH, + capacity=capacity, + ) + ) + if len(live_carriers) > len(point_runs): + _remove_route_carriers(doc, live_carriers[len(point_runs) :]) + _mark_user_path_source_carriers(source, refreshed) + return refreshed + + +def create_user_path_carriers_from_document(doc, project_uuid=""): + """Create or refresh UserPath carriers from all sketch/Draft-like path sources in the document.""" + cleanup_invalid_source_carriers(doc) + created = [] + for source in detect_document_user_path_sources(doc): + if ( + _is_wire_duct_candidate(source) + or _is_support_surface_candidate(source) + or _is_wiring_cut_out_candidate(source) + ): + continue + created.extend(_create_or_refresh_user_path_source(doc, source, project_uuid=project_uuid)) + return created + + def _nearest_points_between_route_point_runs(left_points, right_points): left_points = _normalized_route_points(left_points) right_points = _normalized_route_points(right_points) @@ -2357,6 +2738,8 @@ def _set_route_carrier_source_metadata(carrier, source, source_kind="", source_p def _remember_source_carriers(source, carriers): + if source is None: + return live_names = [ getattr(carrier, "Name", "") for carrier in (carriers or []) @@ -2377,13 +2760,13 @@ def _remember_source_carriers(source, carriers): source_kind=source_kind, source_path_index=source_path_index, ) - TerminalObjects.ensure_string_property( - source, - "QetRouteCarrierNamesJson", - PROPERTY_GROUP, - "Generated route carriers for this source", - json.dumps(live_names, ensure_ascii=False), - ) + TerminalObjects.ensure_string_property( + source, + "QetRouteCarrierNamesJson", + PROPERTY_GROUP, + "Generated route carriers for this source", + json.dumps(live_names, ensure_ascii=False), + ) def _mark_wire_duct_source(source, carrier, carriers=None, end_margin=DEFAULT_WIRE_DUCT_MARGIN): @@ -2457,7 +2840,7 @@ def _mark_user_path_source(source, carrier): def _mark_user_path_source_carriers(source, carriers): carriers = [carrier for carrier in (carriers or []) if carrier is not None] - if source is None or not carriers: + if source is None: return try: _set_user_path_source_semantics(source) @@ -2466,7 +2849,7 @@ def _mark_user_path_source_carriers(source, carriers): "QetRouteCarrierName", PROPERTY_GROUP, "Generated route carrier for this source", - getattr(carriers[0], "Name", ""), + getattr(carriers[0], "Name", "") if carriers else "", ) _remember_source_carriers(source, carriers) except Exception: @@ -2626,6 +3009,47 @@ def detect_user_path_sources(doc): return sources +def _is_document_user_path_source(obj): + if not _is_route_path_source_object(obj): + return False + if _is_explicit_user_path_source(obj): + return True + text = " ".join( + str(value or "") + for value in ( + getattr(obj, "Name", ""), + getattr(obj, "Label", ""), + ) + ).lower() + return any( + keyword in text + for keyword in ( + "userpath", + "user path", + "route path", + "routing path", + "wire path", + "wiring path", + "布线路径", + "走线路径", + "用户路径", + ) + ) + + +def detect_document_user_path_sources(doc): + """Return path sources that are safe to auto-convert during full network generation.""" + sources = [] + seen = set() + for obj in list(getattr(doc, "Objects", []) or []): + if id(obj) in seen: + continue + seen.add(id(obj)) + if _is_document_user_path_source(obj): + sources.append(obj) + return sources + + def _source_sample(obj): return { "name": getattr(obj, "Name", ""), @@ -3060,6 +3484,68 @@ def _terminal_parent_chain(terminal): return chain +def _bbox_volume(bbox): + try: + return ( + max(float(bbox.XMax) - float(bbox.XMin), 0.0) + * max(float(bbox.YMax) - float(bbox.YMin), 0.0) + * max(float(bbox.ZMax) - float(bbox.ZMin), 0.0) + ) + except Exception: + return float("inf") + + +def _terminal_parent_device_bbox(terminal, origin): + candidates = [] + seen = set() + pending = list(getattr(terminal, "InList", []) or []) + while pending: + parent = pending.pop(0) + if parent is None or id(parent) in seen: + continue + seen.add(id(parent)) + bbox = _bound_box_from_object(parent) + if bbox is not None and _point_inside_bbox(origin, bbox, tolerance=DEFAULT_NODE_TOLERANCE): + candidates.append(bbox) + pending.extend(list(getattr(parent, "InList", []) or [])) + if not candidates: + return None + return min(candidates, key=_bbox_volume) + + +def _ray_exit_distance_from_bbox(origin, direction, bbox): + if not _point_inside_bbox(origin, bbox, tolerance=DEFAULT_NODE_TOLERANCE): + return None + distances = [] + for axis in ("x", "y", "z"): + component = _axis_value(direction, axis) + if abs(component) <= DEFAULT_NODE_TOLERANCE: + continue + low, high = _bbox_axis_range(bbox, axis) + boundary = high if component > 0 else low + distance = (boundary - _axis_value(origin, axis)) / component + if distance >= -DEFAULT_NODE_TOLERANCE: + distances.append(max(float(distance), 0.0)) + if not distances: + return None + return min(distances) + + +def _terminal_device_aware_exit_point(terminal, exit_length): + origin = _vector(TerminalObjects.terminal_origin(terminal)) + direction = _normalize(_vector(TerminalObjects.terminal_direction(terminal))) + if direction is None: + direction = App.Vector(0, 0, 1) + + length = max(float(exit_length or 0.0), 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: + # 没有人工局部路径时,默认出线至少先离开所属设备外轮廓,避免导线贴在模型内部。 + length = max(length, exit_distance + DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE) + return _add(origin, _scale(direction, length)) + + def _terminal_local_point_to_global(terminal, local_point): try: if hasattr(terminal, "getGlobalPlacement"): @@ -3176,7 +3662,7 @@ 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_exit_point(terminal, exit_length)]) + return _normalized_route_points([origin, _terminal_device_aware_exit_point(terminal, exit_length)]) def _orthogonal_access_points(start, end): @@ -3491,6 +3977,10 @@ def create_routing_path_network_from_document( selection_ex, project_uuid=project_uuid, ) + document_user_paths = create_user_path_carriers_from_document( + doc, + project_uuid=project_uuid, + ) wire_ducts = create_wire_duct_carriers_from_document( doc, project_uuid=project_uuid, @@ -3520,6 +4010,13 @@ def create_routing_path_network_from_document( for carrier in selected_wire_ducts if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT ) + all_user_paths = [] + seen_user_paths = set() + for carrier in list(selected_user_paths) + list(document_user_paths): + if carrier is None or id(carrier) in seen_user_paths: + continue + seen_user_paths.add(id(carrier)) + all_user_paths.append(carrier) open_end_count = sum( 1 for carrier in all_wire_duct_created @@ -3529,7 +4026,9 @@ def create_routing_path_network_from_document( return { "wire_duct_carriers": wire_duct_main_count, "selected_wire_duct_carriers": selected_wire_duct_main_count, - "user_path_carriers": len(selected_user_paths), + "user_path_carriers": len(all_user_paths), + "selected_user_path_carriers": len(selected_user_paths), + "document_user_path_carriers": len(document_user_paths), "wire_duct_open_end_carriers": open_end_count, "wiring_cut_out_carriers": len(cut_outs), "surface_carriers": len(surfaces), diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 327d7fe..6e321e8 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -150,6 +150,8 @@ class FakeShape: class FakeVertex: + ShapeType = "Vertex" + def __init__(self, point): self.Point = point @@ -197,8 +199,9 @@ class FakeFace: class FakeSelectionItem: - def __init__(self, sub_objects=None, obj=None): + def __init__(self, sub_objects=None, obj=None, sub_element_names=None): self.SubObjects = sub_objects or [] + self.SubElementNames = sub_element_names or [] self.PickedPoints = [] self.Object = obj @@ -2824,7 +2827,7 @@ class AutoRoutingTest(unittest.TestCase): [(point.x, point.y, point.z) for point in carriers[0].Points], ) - def test_selected_user_path_shape_points_honor_object_placement(self): + def test_selected_user_path_shape_points_honor_object_placement_for_non_sketch_path(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") @@ -2833,7 +2836,7 @@ class AutoRoutingTest(unittest.TestCase): doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") - route_path = doc.addObject("Sketcher::SketchObject", "MovedRouteSketch") + route_path = doc.addObject("Part::Feature", "MovedRoutePath") route_path.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) route_path.Shape = FakeShape( FakeBoundBox(0, 50, 0, 50, 20, 20), @@ -2861,6 +2864,296 @@ class AutoRoutingTest(unittest.TestCase): [(point.x, point.y, point.z) for point in carriers[0].Points], ) + def test_selected_sketch_user_path_shape_points_are_not_placement_shifted_twice(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("Sketcher::SketchObject", "PlacedRouteSketch") + route_path.Placement = app.Placement(app.Vector(100, 10, 35), app.Rotation()) + route_path.Shape = FakeShape( + FakeBoundBox(100, 150, 10, 60, 35, 35), + wires=[ + FakeWire( + [ + app.Vector(100, 10, 35), + app.Vector(150, 10, 35), + app.Vector(150, 60, 35), + ] + ) + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual( + [(100.0, 10.0, 35.0), (150.0, 10.0, 35.0), (150.0, 60.0, 35.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) + + def test_controller_creates_face_based_user_path_sketch_for_manual_drawing(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") + panel = doc.addObject("Part::Feature", "MountingPlateA") + face = FakeFace( + FakeBoundBox(0, 100, 0, 60, 10, 10), + app.Vector(0, 0, 1), + vertices=[ + app.Vector(0, 0, 10), + app.Vector(100, 0, 10), + app.Vector(100, 60, 10), + app.Vector(0, 60, 10), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=panel, sub_objects=[face], sub_element_names=["Face1"])], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_path_sketch_from_selection() + sketch = result["sketch"] + + self.assertEqual(1, result["user_path_sketches"]) + self.assertEqual("Sketcher::SketchObject", sketch.TypeId) + self.assertEqual("UserPath", sketch.QetRoutingSourceKind) + self.assertEqual("project-1", sketch.QetProjectUuid) + self.assertEqual("MountingPlateA", sketch.QetRouteSketchSupportName) + self.assertEqual("Face1", sketch.QetRouteSketchSupportSubElement) + self.assertEqual([(panel, "Face1")], sketch.AttachmentSupport) + self.assertEqual("FlatFace", sketch.MapMode) + self.assertEqual(12.0, sketch.Placement.Base.z) + + def test_controller_creates_face_based_user_path_sketch_with_configured_offset(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") + panel = doc.addObject("Part::Feature", "MountingPlateA") + face = FakeFace( + FakeBoundBox(0, 100, 0, 60, 10, 10), + app.Vector(0, 0, 1), + vertices=[ + app.Vector(0, 0, 10), + app.Vector(100, 0, 10), + app.Vector(100, 60, 10), + app.Vector(0, 60, 10), + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=panel, sub_objects=[face], sub_element_names=["Face1"])], + ) + controller = auto_routing_panel.AutoRoutingController() + controller.set_user_path_sketch_offset(50.0) + + result = controller.create_user_path_sketch_from_selection() + sketch = result["sketch"] + + self.assertEqual(60.0, sketch.Placement.Base.z) + self.assertEqual(50.0, sketch.QetRouteSketchFaceOffsetMm) + self.assertEqual(50.0, sketch.AttachmentOffset.Base.z) + + def test_controller_creates_3d_user_path_from_selected_vertices_in_selection_order(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_a = doc.addObject("Part::Feature", "PointA") + source_b = doc.addObject("Part::Feature", "PointB") + source_c = doc.addObject("Part::Feature", "PointC") + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [ + FakeSelectionItem(obj=source_a, sub_objects=[FakeVertex(app.Vector(0, 0, 10))]), + FakeSelectionItem(obj=source_b, sub_objects=[FakeVertex(app.Vector(0, 50, 10))]), + FakeSelectionItem(obj=source_c, sub_objects=[FakeVertex(app.Vector(80, 50, 40))]), + ], + ) + + result = auto_routing_panel.AutoRoutingController().create_user_path_from_selected_points() + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual( + [(0.0, 0.0, 10.0), (0.0, 50.0, 10.0), (80.0, 50.0, 40.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) + + def test_controller_creates_orthogonal_3d_user_path_from_selected_vertices(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_a = doc.addObject("Part::Feature", "PointA") + source_b = doc.addObject("Part::Feature", "PointB") + source_c = doc.addObject("Part::Feature", "PointC") + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [ + FakeSelectionItem(obj=source_a, sub_objects=[FakeVertex(app.Vector(0, 0, 10))]), + FakeSelectionItem(obj=source_b, sub_objects=[FakeVertex(app.Vector(0, 50, 10))]), + FakeSelectionItem(obj=source_c, sub_objects=[FakeVertex(app.Vector(80, 50, 40))]), + ], + ) + + result = auto_routing_panel.AutoRoutingController().create_orthogonal_user_path_from_selected_points() + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(4, result["user_path_point_count"]) + self.assertEqual("Orthogonal3D", carriers[0].QetRoutePathMode) + self.assertEqual( + [ + (0.0, 0.0, 10.0), + (0.0, 50.0, 10.0), + (80.0, 50.0, 10.0), + (80.0, 50.0, 40.0), + ], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) + + def test_generate_routing_paths_discovers_drawn_user_path_sketch_without_selecting_it(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("Sketcher::SketchObject", "DrawnUserPathSketch") + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire( + [ + app.Vector(0, 0, 20), + app.Vector(0, 60, 20), + app.Vector(120, 60, 20), + ] + ) + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [], + ) + + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(1, len(carriers)) + self.assertEqual( + [(0.0, 0.0, 20.0), (0.0, 60.0, 20.0), (120.0, 60.0, 20.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) + + def test_generate_routing_paths_does_not_auto_convert_unmarked_mechanical_sketch(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") + mechanical_sketch = doc.addObject("Sketcher::SketchObject", "Sketch001") + mechanical_sketch.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(120, 0, 20)])], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [], + ) + + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] + + self.assertEqual(0, result["user_path_carriers"]) + self.assertEqual([], carriers) + + def test_marked_user_path_sketch_is_not_skipped_when_name_mentions_support_surface(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("Sketcher::SketchObject", "MountingPlateRouteSketch") + route_path.Label = "安装板布线路径草图" + route_path.QetRoutingSourceKind = "UserPath" + route_path.QetRouteSketchMode = "ManualUserPathSketch" + route_path.Shape = FakeShape( + FakeBoundBox(0, 120, 0, 80, 20, 20), + wires=[ + FakeWire( + [ + app.Vector(0, 0, 20), + app.Vector(0, 80, 20), + app.Vector(120, 80, 20), + ] + ) + ], + ) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [], + ) + + result = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = [ + carrier + for carrier in routing_network.collect_route_carriers(doc) + if carrier.QetRouteCarrierKind == "UserPath" + ] + + self.assertEqual(1, result["user_path_carriers"]) + self.assertEqual(1, len(carriers)) + self.assertEqual( + [(0.0, 0.0, 20.0), (0.0, 80.0, 20.0), (120.0, 80.0, 20.0)], + [(point.x, point.y, point.z) for point in carriers[0].Points], + ) + def test_disconnected_shape_wires_create_separate_user_paths(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -2937,6 +3230,36 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("", carriers[0].QetRouteSourcePathIndex) self.assertEqual(1, len(json.loads(route_path.QetRouteCarrierNamesJson))) + def test_refreshing_marked_user_path_with_no_drawn_lines_removes_stale_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") + route_path = doc.addObject("Sketcher::SketchObject", "EditableUserPathSketch") + route_path.QetRoutingSourceKind = "UserPath" + route_path.QetRouteSketchMode = "ManualUserPathSketch" + route_path.Shape = FakeShape( + FakeBoundBox(0, 80, 0, 20, 20, 20), + wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(80, 20, 20)])], + ) + first = routing_network.create_user_path_carriers_from_document( + doc, + project_uuid="project-1", + ) + route_path.Shape = FakeShape(FakeBoundBox(0, 80, 0, 20, 20, 20), wires=[]) + + second = routing_network.create_user_path_carriers_from_document( + doc, + project_uuid="project-1", + ) + carriers = routing_network.collect_route_carriers(doc) + + self.assertEqual(1, len(first)) + self.assertEqual([], second) + self.assertEqual([], carriers) + self.assertEqual([], json.loads(route_path.QetRouteCarrierNamesJson)) + def test_refreshing_single_wire_user_path_adds_new_carriers_for_added_wires(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -5115,6 +5438,22 @@ class AutoRoutingTest(unittest.TestCase): [(point.x, point.y, point.z) for point in access_points], ) + 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") + 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()