优化自动布线功能流程

dev
邱德佳 3 weeks ago
parent bb30eec80b
commit 0a753ffbd6

@ -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 = <project_uuid>
```
端子空间位置来自 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. 开发验证命令

@ -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 装配和布线状态的真相源。

@ -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

@ -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):

@ -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()
)
)

File diff suppressed because it is too large Load Diff

@ -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"}
)

@ -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
)

@ -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)

Loading…
Cancel
Save