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)