Compare commits

...

23 Commits

Author SHA1 Message Date
Zhaowenlong 2302eea33f feat: 面板支持并行线方向 3 weeks ago
Zhaowenlong 71e2fec765 feat: 面板支持并行线间距 3 weeks ago
Zhaowenlong 02bae59017 fix: 避免容量压力误报 3 weeks ago
Zhaowenlong ae0ecad520 feat: 布线报告提示容量压力 3 weeks ago
Zhaowenlong 1f63e1d72d feat: 记录布线路径载体容量 3 weeks ago
Zhaowenlong d570c75506 feat: 布线报告显示并行错位 3 weeks ago
Zhaowenlong e689d4edf1 feat: 布线报告显示网络调参效果 3 weeks ago
Zhaowenlong 2d7ad273ef feat: 摘要显示线槽桥接数量 3 weeks ago
Zhaowenlong cbd898f2ec feat: 端子未接入诊断显示阈值 3 weeks ago
Zhaowenlong ad24e96695 fix: 检查布线网络使用桥接容差 3 weeks ago
Zhaowenlong e3029f2bd0 feat: 面板支持端子接入参数 3 weeks ago
Zhaowenlong d3b38dc70a feat: 面板支持线槽桥接容差 3 weeks ago
Zhaowenlong 173580594c feat: 支持相邻线槽桥接容差 3 weeks ago
Zhaowenlong 2019f54051 feat: 支持线槽端部缩进配置 3 weeks ago
Zhaowenlong f653549366 feat: 展示布线路径源对象示例 3 weeks ago
Zhaowenlong ea0e0a701d feat: 记录布线轨迹源对象 3 weeks ago
Zhaowenlong 90a3d1786e feat: 支持过线孔桥接距离配置 3 weeks ago
Zhaowenlong 8388f9d15c feat: 扩展过线孔布线桥接 3 weeks ago
Zhaowenlong 4dd33c0365 feat: 清理失效源对象布线载体 3 weeks ago
Zhaowenlong 57fd53dec1 feat: 同步支撑面布线网格数量 3 weeks ago
Zhaowenlong bb7cc7daf2 feat: 刷新过线孔布线路径 3 weeks ago
Zhaowenlong a699bc6d40 docs: 沉淀机柜装配布线关系 3 weeks ago
Zhaowenlong 66502ae505 feat: 刷新支撑面布线网格 3 weeks ago

@ -85,6 +85,14 @@ terminal_uuid
构图时不要求所有 carrier 都提前手工打断。系统会识别轴向线段之间的几何相交和同线重叠,把交点/重叠端点自动切成图节点。这样多条线槽中心路径只要在空间中相交就可以在交点处换向Dijkstra 才能得到符合工程布线习惯的折线路径。
相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝。
FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框手动测试时可直接调整这个选项生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。面板摘要和检查报告都会按当前容差显示自动桥接段数便于确认当前容差是否生效。
同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。
面板还提供“并行线间距 mm”和“并行线方向”用于控制多根导线共用同一路径时的可视 lane 偏移。方向默认 `auto`,也可以手动指定 `x`、`y`、`z`。这些设置只影响 3D 显示上导线之间的错位方式,不代表真实线槽截面内的排布位置。
### 2.1 路由优先级
当前版本按下面优先级处理:
@ -143,6 +151,16 @@ QetProjectUuid = <project_uuid>
Points = [Vector, Vector, ...]
```
由线槽、安装板/柜面、过线孔等源对象自动生成的 carrier 还会记录来源:
```text
QetRouteSourceName = <FreeCAD source object name>
QetRouteSourceLabel = <FreeCAD source object label>
QetRouteSourceKind = "WireDuct" | "RoutingRange" | "WiringCutOut"
```
这些属性只用于 FreeCAD 文档内部刷新和清理,不写入数据库,也不要求 QET 提供。
carrier 统一放在:
```text
@ -185,8 +203,11 @@ QetAutoRouteDiagnosticsJson
QetRoutingSourceKind = "WireDuct"
QetRoutingObstacleMode = "PassThrough"
QetRouteCarrierName = <generated carrier name>
QetWireDuctEndMarginMm = 20.0
```
`QetWireDuctEndMarginMm` 表示自动生成的线槽中心线距离线槽两端缩进多少毫米。默认值用于避开线槽端盖/端部开口;如果某个线槽很短,或现场希望中心路径更靠近端部,可以在 FreeCAD 属性面板中按对象调整。
## 4. 算法设计
### 4.1 路由网络构建
@ -253,6 +274,17 @@ src/Mod/FreeCADExchange/AutoRouting.py
network-dijkstra-v1
```
端子接入有两个可调参数:
```text
terminal_exit_length = 20.0
terminal_access_max_distance = 1000.0
```
`terminal_exit_length` 决定端子出线段长度;`terminal_access_max_distance` 决定端子出线点到最近路由网络的最大允许接入距离。两个参数都只保存在当前 FreeCAD 面板/调用选项中,不写数据库。
网络检查发现端子未接入时,诊断 JSON 会记录该端子到最近路由网络的距离、当前端子接入最大距离和端子出线长度;面板报告会显示当前最大接入距离,便于判断是设备/线槽位置还没摆好,还是需要临时调大接入阈值。
### 4.4 悬空线策略
当前版本默认:
@ -324,6 +356,28 @@ src/Mod/FreeCADExchange/InitGui.py
生成线槽 carrier 时,系统除了 `WireDuct` 中心路径,还会在线槽两端生成 `WireDuctOpenEnd` 横向路径;对象名或标签包含 `Wiring Cut-Out`、`wire cutout`、`穿线孔`、`过线孔` 等语义时,会生成 `WiringCutOut` 穿线路径载体。
`WiringCutOut` 不是按开孔实体外轮廓布线,而是按开孔包围盒最薄方向生成一条虚拟穿线路径,并在穿孔方向两端做小距离外扩。这样安装板、隔板或线槽侧壁上的孔可以接到孔两侧附近的线槽中心线或支撑面网格,避免路径只停留在板厚范围内而无法连通网络。
过线孔源对象可通过下面的 FreeCAD 属性调整外扩距离:
```text
QetWiringCutOutBridgeExtensionMm = 20.0
```
该属性表示在穿孔方向每一侧额外延长多少毫米。默认值用于常见线槽中心线与板面/孔位有少量间距的情况;如果现场模型中线槽中心离开孔更近或更远,可以在 FreeCAD 属性面板里按对象单独调整。
自动生成的 carrier 会随源对象生命周期刷新:源对象仍有效时更新几何;安装板尺寸变化时同步增删 `RoutingRange` 网格线;源对象被删除或不再满足线槽/支撑面规则时,下一次生成布线路径网络会删除对应自动 carrier并撤销该源对象的穿越/支撑面障碍模式。用户手工创建、没有源对象元数据的 carrier 不会被这一步自动删除。
生成导线的 `QetRouteTrackJson` 会记录实际经过的 carrier。carrier 如果来自线槽、过线孔、支撑面或端子接入源对象route track 中还会保留 `source_name`、`source_label`、`source_kind`,用于手动测试时追踪“这段线实际走过哪个 3D 源对象”。route track 同时记录 carrier 的 `capacity`,用于后续核对多根线共路、容量偏好和绕行行为。
批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。
批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻线槽自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。
如果多条导线共用同一路径并触发 lane 偏移,批量报告会显示最大 lane 编号和 lane 间距。这个值用于确认当前结果是否只是完全重叠的导线,还是已经按共路情况做了可视错位;它仍然是显示层偏移,不等于真实线槽截面排布或填充率计算。
当单条路线的最大并行线数超过该路线 route track 中记录的路径最小容量时,批量报告会给出容量提示。这个提示只基于 `QetRouteCarrierCapacity` 和当前 lane 情况,用于暴露“可能容量不足”的调试线索,不等同于按线径、截面积和线槽填充率计算的工程容量校核。
### 5.3 布线连接功能
已完成:
@ -395,6 +449,20 @@ tests/python/freecad_exchange_auto_routing_test.py
21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。
22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。
23. 导线会保存 routing track网络检查会生成 `RoutingPathNetwork` 诊断对象。
24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。
25. `WiringCutOut` 会在穿孔方向外扩虚拟路径,用于桥接开孔两侧附近的线槽或支撑面网络,并支持通过 `QetWiringCutOutBridgeExtensionMm` 按对象调整外扩距离。
26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。
27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。
28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。
29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。
30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。
31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。
32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。
33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。
34. 批量布线报告会显示最大 lane 编号和 lane 间距,方便确认多根线共路时是否发生了可视错位。
35. `QetRouteTrackJson` 的 carrier payload 会记录 `capacity`,方便后续分析线槽容量偏好和共路绕行。
36. 批量布线报告会在最大并行线数超过路径最小容量时显示容量提示,但当前仍不做真实填充率计算。
37. `3D 布线连接` 面板提供“并行线间距 mm”和“并行线方向”设置用于调整多线共路时的可视 lane 偏移。
已完成 FreeCAD smoke
@ -414,9 +482,10 @@ tests/manual/freecad_auto_routing_smoke.py
3. 清除布线连接
4. 清除走线路径
5. 点击“准备布线布局空间”
6. 可选:选中无法自动识别的线槽实体
7. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别
8. 点击“生成布线连接”
6. 按当前机柜情况调整线槽桥接容差、端子接入最大距离、端子出线长度
7. 可选:选中无法自动识别的线槽实体
8. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别
9. 点击“生成布线连接”
```
三个按钮的职责:
@ -448,19 +517,40 @@ end_terminal_display
注意:批量生成布线连接的依据是导线任务,不是“所有端子自动互连”。如果文档中只有端子而没有 `wires[]``QETWiring_01_Tasks`,系统不能判断哪些端子应该连接。
### 6.3 现场机柜资料对自动布线的约定
根据本地测试机柜、甲方布线操作视频和安装板/导轨/设备位置关系视频,第一版自动布线需要按真实机柜装配习惯理解对象:
1. 柜体和框架主要提供结构边界,默认作为障碍或场景参考,不作为导线可走路径。
2. 安装板是柜体结构的一部分,通常通过螺丝孔、加强梁或连接件固定在柜体内,不能理解为悬空对象。
3. 安装板、背板、门板等薄板可作为低优先级 `RoutingRange` 支撑面;它们用于没有线槽或线槽不完整时的贴面过渡,不应优先于线槽。
4. DIN 导轨固定在安装板或梁上,是设备安装基准,不是导线主路径。导轨自身不应被自动识别成线槽。
5. 设备不能悬空,应装在导轨或安装板上。自动布线只消费设备最终 3D 位姿、工程端子位置和端子出线方向。
6. 线槽是导线主路径。导线应优先从设备端子经 `TerminalAccess` 进入线槽,再沿 `WireDuct` 网络到达另一端。
7. 过线孔/穿线孔用于连接不同安装面、线槽或柜体开孔处的网络,应建模为 `WiringCutOut`,不是普通障碍。
因此,自动布线的推荐空间语义是:
```text
工程端子 -> TerminalAccess -> WireDuct / WiringCutOut / RoutingRange -> TerminalAccess -> 工程端子
```
安装板和导轨的机械配合关系会影响对象最终位置,但当前路由器不从装配约束求解位置;它只读取 FreeCAD 文档中已经确定的几何位姿。后续如果增加装配语义,应保存在 FreeCAD 文档中,不扩展第一版数据库绑定表。
## 7. 当前限制
当前版本可完成布线连接原型,但仍有以下限制:
1. 线槽实体中心线生成基于包围盒长轴,不理解真实线槽开口、盖板和内部空间。
2. 多根线会沿同一路径生成,暂未做并行错位排列。
3. 未计算线槽填充率和容量。
2. 多根线共路时已做基础错位显示,但不是线束级排布,也不计算每根线在线槽内的真实截面位置
3. 已支持简单路径容量属性和超容量避让,但未按线径、截面积、填充率计算真实线槽容量。
4. 未考虑线径、最小弯曲半径。
5. 未做强弱电分槽、线缆类型隔离。
6. 障碍检测基于 AABB存在误报和漏报。
7. 辅助路由区域是网格近似,不等于专业软件的完整布线区域建模。
8. 端子出线方向依赖端子 LCS 方向;如果模板端子方向不准,布线连接会受影响。
9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。
10. 当前不自动求解导轨、安装板和设备之间的 Assembly 配合关系;装配位置以 `scene.FCStd` 中对象的最终 `Placement` 为准。
## 8. 后续需要完成

@ -364,6 +364,34 @@ Z = 1200 mm
不要选择 `Gears`。导轨不是运动部件。
### 7.4 现场机柜中的配合关系
从现场沟通和安装板/导轨/设备位置关系视频看,柜内对象不是“飘在空间里”的独立几何,而是通过机械配合关系装到柜体内:
| 对象 | 常见宿主 | 典型配合关系 | 对布线的意义 |
| --- | --- | --- | --- |
| 安装板 | 柜体框架、背板梁、连接件 | 平面贴合、平行、距离、螺丝孔对齐 | 可作为低优先级布线支撑面 `RoutingRange` |
| DIN 导轨 | 安装板、梁 | 背面贴合、平行、距离、孔位固定 | 作为设备安装基准,不作为导线主路径 |
| 线槽 | 安装板、柜内侧边、梁 | 底面贴合、平行、距离、螺丝孔固定 | 作为导线主路径 `WireDuct` |
| 设备 | DIN 导轨或安装板 | 卡扣贴合、面贴合、孔位固定、固定间距排列 | 提供工程端子位置和出线方向 |
| 过线孔/穿线孔 | 安装板、柜体隔板 | 与开孔同轴或共面 | 作为跨区域路径 `WiringCutOut` |
当前 FreeCADExchange 的能力边界:
1. FreeCAD 原生 `Assembly` 工作台可以做平面对齐、距离、同轴/共线等配合关系。
2. FreeCADExchange 目前主要保存对象最终 `Placement`,并提供轻量的 `贴合到选中面` 辅助。
3. `贴合到选中面` 是一次性位姿调整,不是持久 Assembly 约束求解器。
4. 当前不会自动保存“设备装在哪根导轨上”“导轨固定在哪块安装板上”这类完整宿主关系。
5. 自动布线读取 `scene.FCStd` 里的最终几何位置,不从装配约束反推位置。
推荐建模习惯:
1. 先把柜体或安装板固定好。
2. 导轨、线槽都贴合到安装板或柜体内部结构上。
3. 设备必须装到导轨或安装板上,避免悬空。
4. 装配调整后重新生成布线路径网络,让 `WireDuct`、`RoutingRange` 和 `TerminalAccess` 跟随最新位置刷新。
5. 如果后续要做自动装配,应优先在 FreeCAD 文档内增加宿主语义,例如 `Cabinet`、`MountingPlate`、`DINRail`、`WireDuct`、`Device`,不要扩展第一版数据库绑定表。
---
## 8. 放置线槽

@ -29,6 +29,7 @@ DEFAULT_OPTIONS = {
# 线槽网络相关参数。
"use_routing_network": True,
"network_entry_max_distance": 1000.0,
"adjoining_duct_tolerance": RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE,
"bend_penalty": 25.0,
# EPLAN/SOLIDWORKS 风格:线槽/路由路径最优先,辅助面域只作为过渡/兜底区域。
"carrier_kind_cost_factors": {
@ -824,6 +825,7 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non
"network": {
"carriers": int(network.get("carrier_count", 0)),
"segments": int(network.get("segment_count", 0)),
"bridged_segments": int(network.get("bridged_segment_count", 0)),
"blocked_segments": int(network.get("blocked_segment_count", 0)),
"nodes": len(network.get("nodes", {})),
"entry_distance": float(start_distance or 0.0),
@ -843,12 +845,19 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non
blocked_bboxes = [obstacle["bbox"] for obstacle in obstacles if obstacle.get("bbox")]
if blocked_bboxes:
obstacle_aware_network = RoutingNetwork.build_route_graph(doc, blocked_bboxes=blocked_bboxes)
obstacle_aware_network = RoutingNetwork.build_route_graph(
doc,
blocked_bboxes=blocked_bboxes,
adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0),
)
route_data = route_on_network(obstacle_aware_network, obstacle_aware=True)
if route_data is not None:
return route_data
network = RoutingNetwork.build_route_graph(doc)
network = RoutingNetwork.build_route_graph(
doc,
adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0),
)
return route_on_network(network, obstacle_aware=False)
@ -1689,6 +1698,127 @@ def _endpoint_pair_text(sample):
)
def _route_source_labels(route_track, limit=5):
labels = []
seen = set()
if not isinstance(route_track, dict):
return labels
for segment in route_track.get("segments", []) or []:
carrier = segment.get("carrier", {}) if isinstance(segment, dict) else {}
if not isinstance(carrier, dict):
continue
label = (
str(carrier.get("source_label", "") or "").strip()
or str(carrier.get("source_name", "") or "").strip()
)
if not label or label in seen:
continue
seen.add(label)
labels.append(label)
if len(labels) >= int(limit or 0):
break
return labels
def _route_source_sample_text(report):
for route in report.get("routes", []) or []:
if not isinstance(route, dict):
continue
labels = _route_source_labels(route.get("route_track", {}))
if not labels:
continue
return "路径示例:导线 {0} 经过 {1}".format(
_wire_sample_text(route),
"".join(labels),
)
return ""
def _route_network_metric_max(report, key):
maximum = 0
for route in report.get("routes", []) or []:
if not isinstance(route, dict):
continue
network = route.get("network", {})
if not isinstance(network, dict):
continue
try:
maximum = max(maximum, int(network.get(key, 0) or 0))
except Exception:
continue
return maximum
def _route_lane_summary(report):
max_lane_index = 0
lane_spacing = 0.0
for route in report.get("routes", []) or []:
if not isinstance(route, dict):
continue
lane = route.get("lane", {})
if not isinstance(lane, dict):
continue
try:
lane_index = int(lane.get("index", 0) or 0)
except Exception:
lane_index = 0
if lane_index <= max_lane_index:
continue
max_lane_index = lane_index
try:
lane_spacing = float(lane.get("spacing_mm", 0.0) or 0.0)
except Exception:
lane_spacing = 0.0
if max_lane_index <= 0:
return {}
return {
"max_lane_index": max_lane_index,
"spacing_mm": lane_spacing,
}
def _route_track_min_capacity(route_track):
if not isinstance(route_track, dict):
return None
capacities = []
for segment in route_track.get("segments", []) or []:
carrier = segment.get("carrier", {}) if isinstance(segment, dict) else {}
if not isinstance(carrier, dict):
continue
try:
capacity = int(float(carrier.get("capacity", 0) or 0))
except Exception:
capacity = 0
if capacity > 0:
capacities.append(capacity)
if not capacities:
return None
return min(capacities)
def _route_capacity_pressure_summary(report):
pressure = {}
for route in report.get("routes", []) or []:
if not isinstance(route, dict):
continue
lane = route.get("lane", {})
if not isinstance(lane, dict):
continue
try:
max_parallel_wires = int(lane.get("index", 0) or 0) + 1
except Exception:
max_parallel_wires = 1
route_capacity = _route_track_min_capacity(route.get("route_track", {}))
if route_capacity is None or max_parallel_wires <= route_capacity:
continue
if not pressure or max_parallel_wires > pressure.get("max_parallel_wires", 0):
pressure = {
"max_parallel_wires": max_parallel_wires,
"min_capacity": route_capacity,
}
return pressure
def format_eplan_connection_route_report(report):
message = "批量生成布线连接完成routed={0}, collision_warnings={1}, missing_terminals={2}".format(
report.get("routed", 0),
@ -1731,6 +1861,30 @@ def format_eplan_connection_route_report(report):
total_length_mm = float(report.get("total_length_mm", 0.0) or 0.0)
if total_length_mm > 0.0:
message += "\n布线连接总长度:{0:.1f} mm。".format(total_length_mm)
bridged_segments = _route_network_metric_max(report, "bridged_segments")
blocked_segments = _route_network_metric_max(report, "blocked_segments")
network_parts = []
if bridged_segments > 0:
network_parts.append("自动桥接 {0} 段相邻线槽".format(bridged_segments))
if blocked_segments > 0:
network_parts.append("避障屏蔽 {0}".format(blocked_segments))
if network_parts:
message += "\n路径网络:{0}".format("".join(network_parts))
lane_summary = _route_lane_summary(report)
if lane_summary:
message += "\n并行错位:最大 lane {0},间距 {1:.1f} mm。".format(
lane_summary.get("max_lane_index", 0),
float(lane_summary.get("spacing_mm", 0.0) or 0.0),
)
capacity_pressure = _route_capacity_pressure_summary(report)
if capacity_pressure:
message += "\n容量提示:最大并行线数 {0},路径最小容量 {1}".format(
capacity_pressure.get("max_parallel_wires", 0),
capacity_pressure.get("min_capacity", 0),
)
route_source_sample = _route_source_sample_text(report)
if route_source_sample:
message += "\n{0}".format(route_source_sample)
errors = report.get("errors", []) or []
if errors:
message += "\n首个错误:{0}".format(str(errors[0]))
@ -1932,6 +2086,7 @@ def generate_eplan_routing_path_network(doc, project_uuid="", options=None, sele
selection_ex=selection_ex,
terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0),
terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0),
adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0),
)
@ -1951,6 +2106,7 @@ def check_eplan_routing_path_network(doc, project_uuid="", options=None):
project_uuid=target_project_uuid,
terminal_exit_length=float(opts.get("terminal_exit_length", 20.0) or 0.0),
terminal_access_max_distance=float(opts.get("terminal_access_max_distance", 1000.0) or 0.0),
adjoining_duct_tolerance=float(opts.get("adjoining_duct_tolerance", 0.0) or 0.0),
)
diagnostic = result.get("diagnostic", {}) if isinstance(result, dict) else {}
return {
@ -2006,19 +2162,24 @@ def format_routing_path_network_report(diagnostic):
summary = diagnostic.get("summary", {}) if isinstance(diagnostic.get("summary", {}), dict) else {}
issues = _dict_items(diagnostic.get("issues", []) or [])
if not issues:
return "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。".format(
message = "布线路径网络检查通过:{0} 条 carrier / {1} 段 / {2} 个节点。".format(
summary.get("carriers", 0),
summary.get("segments", 0),
summary.get("nodes", 0),
)
bridged_segments = int(summary.get("bridged_segments", 0) or 0)
if bridged_segments > 0:
message += " 自动桥接 {0} 段相邻线槽。".format(bridged_segments)
return message
message = "布线路径网络检查发现 {0} 类问题。".format(len(issues))
unconnected = _dict_items(diagnostic.get("unconnected_terminals", []) or [])
if unconnected:
sample = unconnected[0]
message += "\n端子未接入:{0},距离最近网络 {1}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format(
message += "\n端子未接入:{0},距离最近网络 {1},当前端子接入最大距离 {2}。请重新生成布线路径网络,或补一段线槽/辅助路径到该端子。".format(
_diagnostic_terminal_text(sample),
_format_distance_mm(sample.get("nearest_network_distance_mm")),
_format_distance_mm(sample.get("terminal_access_max_distance_mm")),
)
possible_breaks = _dict_items(diagnostic.get("possible_breaks", []) or [])

@ -71,8 +71,46 @@ def _selection_ex():
class AutoRoutingController:
def __init__(self):
def __init__(self, options=None):
self.last_report = None
self.options = dict(options or {})
def routing_options(self):
return dict(self.options)
def set_adjoining_duct_tolerance(self, value):
try:
tolerance = float(value)
except Exception:
tolerance = RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE
self.options["adjoining_duct_tolerance"] = max(tolerance, 0.0)
def set_terminal_access_max_distance(self, value):
try:
max_distance = float(value)
except Exception:
max_distance = RoutingNetwork.DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE
self.options["terminal_access_max_distance"] = max(max_distance, 0.0)
def set_terminal_exit_length(self, value):
try:
exit_length = float(value)
except Exception:
exit_length = AutoRouting.DEFAULT_OPTIONS["terminal_exit_length"]
self.options["terminal_exit_length"] = max(exit_length, 0.0)
def set_lane_spacing(self, value):
try:
lane_spacing = float(value)
except Exception:
lane_spacing = AutoRouting.DEFAULT_OPTIONS["lane_spacing"]
self.options["lane_spacing"] = max(lane_spacing, 0.0)
def set_lane_axis(self, value):
lane_axis = str(value or "").strip().lower()
if lane_axis not in {"auto", "x", "y", "z"}:
lane_axis = AutoRouting.DEFAULT_OPTIONS["lane_axis"]
self.options["lane_axis"] = lane_axis
def summary(self):
doc = _active_document()
@ -82,7 +120,16 @@ class AutoRoutingController:
payload_wire_count = 0
if isinstance(payload, dict) and isinstance(payload.get("wires"), list):
payload_wire_count = len(payload.get("wires") or [])
network = RoutingNetwork.network_summary(doc)
network = RoutingNetwork.network_summary(
doc,
adjoining_duct_tolerance=float(
self.routing_options().get(
"adjoining_duct_tolerance",
RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE,
)
or 0.0
),
)
kinds = network.get("kinds", {}) if isinstance(network.get("kinds", {}), dict) else {}
kind_text = ""
if kinds:
@ -90,7 +137,10 @@ class AutoRoutingController:
"{0}={1}".format(key, value)
for key, value in sorted(kinds.items())
)
return "端子:{0};导线任务:{1}QET导线{2};路由网络:{3} 条 carrier / {4} 段 / {5} 节点{6}".format(
bridge_text = ""
if int(network.get("bridged_segments", 0) or 0) > 0:
bridge_text = ";桥接:{0}".format(network.get("bridged_segments", 0))
return "端子:{0};导线任务:{1}QET导线{2};路由网络:{3} 条 carrier / {4} 段 / {5} 节点{6}{7}".format(
terminal_count,
task_count,
payload_wire_count,
@ -98,6 +148,7 @@ class AutoRoutingController:
network.get("segments", 0),
network.get("nodes", 0),
kind_text,
bridge_text,
)
def generate_routing_paths(self):
@ -109,6 +160,7 @@ class AutoRoutingController:
self.last_report = AutoRouting.generate_eplan_routing_path_network(
doc,
project_uuid=project_uuid,
options=self.routing_options(),
selection_ex=selection_ex,
)
self.last_report["source_mode"] = source_mode
@ -120,6 +172,7 @@ class AutoRoutingController:
self.last_report = AutoRouting.check_eplan_routing_path_network(
doc,
project_uuid=project_uuid,
options=self.routing_options(),
)
return self.last_report
@ -141,6 +194,7 @@ class AutoRoutingController:
report = AutoRouting.route_eplan_connections(
doc,
payload=payload if isinstance(payload, dict) and payload.get("wires") else None,
options=self.routing_options(),
project_uuid=project_uuid,
update_network=True,
)
@ -183,6 +237,77 @@ class AutoRoutingTaskPanel:
layout = QtWidgets.QVBoxLayout(self.form)
options_layout = QtWidgets.QHBoxLayout()
options_layout.addWidget(QtWidgets.QLabel("线槽桥接容差 mm"))
self.adjoining_duct_tolerance_spin = QtWidgets.QDoubleSpinBox()
self.adjoining_duct_tolerance_spin.setRange(0.0, 1000.0)
self.adjoining_duct_tolerance_spin.setDecimals(1)
self.adjoining_duct_tolerance_spin.setSingleStep(1.0)
self.adjoining_duct_tolerance_spin.setValue(
float(
self.controller.routing_options().get(
"adjoining_duct_tolerance",
RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE,
)
)
)
options_layout.addWidget(self.adjoining_duct_tolerance_spin)
options_layout.addWidget(QtWidgets.QLabel("端子接入最大距离 mm"))
self.terminal_access_max_distance_spin = QtWidgets.QDoubleSpinBox()
self.terminal_access_max_distance_spin.setRange(0.0, 100000.0)
self.terminal_access_max_distance_spin.setDecimals(1)
self.terminal_access_max_distance_spin.setSingleStep(50.0)
self.terminal_access_max_distance_spin.setValue(
float(
self.controller.routing_options().get(
"terminal_access_max_distance",
RoutingNetwork.DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE,
)
)
)
options_layout.addWidget(self.terminal_access_max_distance_spin)
options_layout.addWidget(QtWidgets.QLabel("端子出线长度 mm"))
self.terminal_exit_length_spin = QtWidgets.QDoubleSpinBox()
self.terminal_exit_length_spin.setRange(0.0, 1000.0)
self.terminal_exit_length_spin.setDecimals(1)
self.terminal_exit_length_spin.setSingleStep(5.0)
self.terminal_exit_length_spin.setValue(
float(
self.controller.routing_options().get(
"terminal_exit_length",
AutoRouting.DEFAULT_OPTIONS["terminal_exit_length"],
)
)
)
options_layout.addWidget(self.terminal_exit_length_spin)
options_layout.addWidget(QtWidgets.QLabel("并行线间距 mm"))
self.lane_spacing_spin = QtWidgets.QDoubleSpinBox()
self.lane_spacing_spin.setRange(0.0, 1000.0)
self.lane_spacing_spin.setDecimals(1)
self.lane_spacing_spin.setSingleStep(1.0)
self.lane_spacing_spin.setValue(
float(
self.controller.routing_options().get(
"lane_spacing",
AutoRouting.DEFAULT_OPTIONS["lane_spacing"],
)
)
)
options_layout.addWidget(self.lane_spacing_spin)
options_layout.addWidget(QtWidgets.QLabel("并行线方向"))
self.lane_axis_combo = QtWidgets.QComboBox()
self.lane_axis_combo.addItems(["auto", "x", "y", "z"])
lane_axis = str(
self.controller.routing_options().get(
"lane_axis",
AutoRouting.DEFAULT_OPTIONS["lane_axis"],
)
or "auto"
).lower()
axis_index = self.lane_axis_combo.findText(lane_axis)
self.lane_axis_combo.setCurrentIndex(axis_index if axis_index >= 0 else 0)
options_layout.addWidget(self.lane_axis_combo)
self.generate_layout_button = QtWidgets.QPushButton("准备布线布局空间")
self.generate_layout_button.setToolTip(
"按 EPLAN 布局空间语义识别线槽、安装面、工程端子和障碍处理方式,不生成导线。"
@ -221,6 +346,7 @@ class AutoRoutingTaskPanel:
):
layout.addWidget(widget)
layout.addLayout(options_layout)
layout.addWidget(self.status_label)
self.generate_paths_button.clicked.connect(self.generate_routing_paths)
@ -247,8 +373,16 @@ class AutoRoutingTaskPanel:
self.status_label.setText(message)
_console_error(message)
def _sync_options_from_widgets(self):
self.controller.set_adjoining_duct_tolerance(self.adjoining_duct_tolerance_spin.value())
self.controller.set_terminal_access_max_distance(self.terminal_access_max_distance_spin.value())
self.controller.set_terminal_exit_length(self.terminal_exit_length_spin.value())
self.controller.set_lane_spacing(self.lane_spacing_spin.value())
self.controller.set_lane_axis(self.lane_axis_combo.currentText())
def generate_routing_paths(self):
try:
self._sync_options_from_widgets()
result = self.controller.generate_routing_paths()
wire_ducts = result.get("wire_duct_carriers", 0)
surfaces = result.get("surface_carriers", 0)
@ -274,6 +408,7 @@ class AutoRoutingTaskPanel:
def check_routing_path_network(self):
try:
self._sync_options_from_widgets()
result = self.controller.check_routing_path_network()
diagnostic = result.get("diagnostic", {}) if isinstance(result.get("diagnostic", {}), dict) else {}
self._set_status(
@ -310,6 +445,7 @@ class AutoRoutingTaskPanel:
def route_eplan_connections(self):
try:
self._sync_options_from_widgets()
report = self.controller.route_eplan_connections()
self._set_status(AutoRouting.format_eplan_connection_route_report(report))
except Exception as exc:

@ -22,6 +22,11 @@ ROUTE_CARRIER_KIND_WIRING_CUT_OUT = "WiringCutOut"
ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath"
ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange"
ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess"
MANAGED_ROUTE_SOURCE_KINDS = {
ROUTE_CARRIER_KIND_WIRE_DUCT,
ROUTE_CARRIER_KIND_WIRING_CUT_OUT,
ROUTE_CARRIER_KIND_ROUTING_RANGE,
}
PROPERTY_GROUP = "QET Routing"
DEFAULT_NODE_TOLERANCE = 0.001
DEFAULT_SURFACE_LANE_SPACING = 100.0
@ -33,6 +38,7 @@ DEFAULT_ROUTE_PATH_FACE_OFFSET = 2.0
DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT = 2.5
DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE = 1000.0
DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0
DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0
WIRE_DUCT_OBSTACLE_MODE = "PassThrough"
SUPPORT_SURFACE_OBSTACLE_MODE = "SupportSurface"
WIRE_DUCT_NAME_KEYWORDS = (
@ -423,6 +429,30 @@ def _ensure_integer_property(obj, prop_name, description, value):
setattr(obj, prop_name, 0)
def _ensure_float_property(obj, prop_name, description, value):
if prop_name not in getattr(obj, "PropertiesList", []):
obj.addProperty(
"App::PropertyFloat",
prop_name,
PROPERTY_GROUP,
description,
)
try:
setattr(obj, prop_name, float(value))
except Exception:
setattr(obj, prop_name, 0.0)
def _wiring_cut_out_bridge_extension_value(source, default=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION):
try:
value = float(getattr(source, "QetWiringCutOutBridgeExtensionMm", default) or 0.0)
except Exception:
value = float(default or 0.0)
if value < 0.0:
return 0.0
return value
def _set_route_carrier_semantics(obj, project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1):
TerminalObjects.ensure_string_property(
obj,
@ -472,7 +502,17 @@ def _route_carrier_capacity_value(obj, default=1):
return int(default or 1)
def _set_wire_duct_source_semantics(source):
def _wire_duct_end_margin_value(source, default=DEFAULT_WIRE_DUCT_MARGIN):
try:
value = float(getattr(source, "QetWireDuctEndMarginMm", default) or 0.0)
except Exception:
value = float(default or 0.0)
if value < 0.0:
return 0.0
return value
def _set_wire_duct_source_semantics(source, end_margin=DEFAULT_WIRE_DUCT_MARGIN):
if source is None:
return
TerminalObjects.ensure_string_property(
@ -495,6 +535,12 @@ def _set_wire_duct_source_semantics(source):
"How many routed wires can reuse generated wire duct segments before detouring is preferred",
_route_carrier_capacity_value(source, default=1),
)
_ensure_float_property(
source,
"QetWireDuctEndMarginMm",
"How far generated wire duct centerlines stay inside each duct end",
_wire_duct_end_margin_value(source, default=end_margin),
)
def _set_support_surface_source_semantics(source):
@ -516,7 +562,7 @@ def _set_support_surface_source_semantics(source):
)
def _set_wiring_cut_out_source_semantics(source):
def _set_wiring_cut_out_source_semantics(source, bridge_extension=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION):
if source is None:
return
TerminalObjects.ensure_string_property(
@ -533,6 +579,12 @@ def _set_wiring_cut_out_source_semantics(source):
"How routing connection collision checks should treat this object",
WIRE_DUCT_OBSTACLE_MODE,
)
_ensure_float_property(
source,
"QetWiringCutOutBridgeExtensionMm",
"How far the generated wiring cut-out carrier extends beyond each side of the opening",
_wiring_cut_out_bridge_extension_value(source, default=bridge_extension),
)
def _style_route_carrier(carrier, kind):
@ -872,10 +924,11 @@ def _detach_from_groups(doc, obj):
pass
def clear_route_carriers(doc):
"""Delete generated route carriers while keeping terminals and routed wires."""
def _remove_route_carriers(doc, carriers):
removed = 0
for carrier in list(collect_route_carriers(doc)):
for carrier in list(carriers or []):
if carrier is None or not is_route_carrier(carrier):
continue
_detach_from_groups(doc, carrier)
try:
if doc.getObject(getattr(carrier, "Name", "")) is not None:
@ -883,6 +936,12 @@ def clear_route_carriers(doc):
removed += 1
except Exception:
pass
return removed
def clear_route_carriers(doc):
"""Delete generated route carriers while keeping terminals and routed wires."""
removed = _remove_route_carriers(doc, collect_route_carriers(doc))
try:
doc.recompute()
except Exception:
@ -1420,7 +1479,14 @@ def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_a
).get("centerline", [])
def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity=1):
def _sync_wire_duct_source_carriers(
doc,
source,
spec,
project_uuid="",
capacity=1,
end_margin=DEFAULT_WIRE_DUCT_MARGIN,
):
carriers = _live_source_carriers(doc, source)
if not carriers:
return False
@ -1446,7 +1512,7 @@ def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity
updated.append(carrier)
if updated:
_mark_wire_duct_source(source, updated[0], updated)
_mark_wire_duct_source(source, updated[0], updated, end_margin=end_margin)
try:
doc.recompute()
except Exception:
@ -1454,7 +1520,7 @@ def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity
return True
def _wiring_cut_out_points_from_bbox(bbox):
def _wiring_cut_out_points_from_bbox(bbox, bridge_extension=0.0):
extents = _bbox_extents(bbox)
if not extents:
return []
@ -1470,6 +1536,9 @@ def _wiring_cut_out_points_from_bbox(bbox):
fallback = max(other_extents or [DEFAULT_WIRE_DUCT_OPEN_END_MIN_LENGTH])
low = _axis_value(center, through_axis) - fallback * 0.5
high = _axis_value(center, through_axis) + fallback * 0.5
extension = max(float(bridge_extension or 0.0), 0.0)
low -= extension
high += extension
start = _set_axis(center, through_axis, low)
end = _set_axis(center, through_axis, high)
if _distance(start, end) <= DEFAULT_NODE_TOLERANCE:
@ -1528,6 +1597,40 @@ def _live_source_carriers(doc, source):
return carriers
def _source_kind_value(source):
return (getattr(source, "QetRoutingSourceKind", "") or "").strip()
def _set_route_carrier_source_metadata(carrier, source, source_kind=""):
if carrier is None or source is None:
return
source_name = (getattr(source, "Name", "") or "").strip()
if not source_name:
return
kind = (source_kind or _source_kind_value(source)).strip()
TerminalObjects.ensure_string_property(
carrier,
"QetRouteSourceName",
PROPERTY_GROUP,
"FreeCAD source object name that generated this route carrier",
source_name,
)
TerminalObjects.ensure_string_property(
carrier,
"QetRouteSourceLabel",
PROPERTY_GROUP,
"FreeCAD source object label that generated this route carrier",
getattr(source, "Label", "") or source_name,
)
TerminalObjects.ensure_string_property(
carrier,
"QetRouteSourceKind",
PROPERTY_GROUP,
"Routing source kind that generated this route carrier",
kind,
)
def _remember_source_carriers(source, carriers):
live_names = [
getattr(carrier, "Name", "")
@ -1535,6 +1638,9 @@ def _remember_source_carriers(source, carriers):
if carrier is not None and getattr(carrier, "Name", "")
]
if live_names:
source_kind = _source_kind_value(source)
for carrier in carriers or []:
_set_route_carrier_source_metadata(carrier, source, source_kind=source_kind)
TerminalObjects.ensure_string_property(
source,
"QetRouteCarrierNamesJson",
@ -1544,11 +1650,11 @@ def _remember_source_carriers(source, carriers):
)
def _mark_wire_duct_source(source, carrier, carriers=None):
def _mark_wire_duct_source(source, carrier, carriers=None, end_margin=DEFAULT_WIRE_DUCT_MARGIN):
if source is None:
return
try:
_set_wire_duct_source_semantics(source)
_set_wire_duct_source_semantics(source, end_margin=end_margin)
if carrier is not None:
TerminalObjects.ensure_string_property(
source,
@ -1574,15 +1680,16 @@ def _mark_support_surface_source(source, carriers):
"Generated route carrier for this source",
getattr(carriers[0], "Name", ""),
)
_remember_source_carriers(source, carriers)
except Exception:
pass
def _mark_wiring_cut_out_source(source, carrier):
def _mark_wiring_cut_out_source(source, carrier, bridge_extension=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION):
if source is None or carrier is None:
return
try:
_set_wiring_cut_out_source_semantics(source)
_set_wiring_cut_out_source_semantics(source, bridge_extension=bridge_extension)
TerminalObjects.ensure_string_property(
source,
"QetRouteCarrierName",
@ -1590,6 +1697,7 @@ def _mark_wiring_cut_out_source(source, carrier):
"Generated route carrier for this source",
getattr(carrier, "Name", ""),
)
_remember_source_carriers(source, [carrier])
except Exception:
pass
@ -1612,6 +1720,7 @@ def _mark_terminal_access_source(source, carrier):
"Generated route carrier for this source",
getattr(carrier, "Name", ""),
)
_remember_source_carriers(source, [carrier])
except Exception:
pass
@ -1621,6 +1730,77 @@ def _live_source_carrier(doc, source):
return carriers[0] if carriers else None
def _source_is_valid_for_kind(source, source_kind):
if source_kind == ROUTE_CARRIER_KIND_WIRE_DUCT:
return _is_wire_duct_candidate(source)
if source_kind == ROUTE_CARRIER_KIND_ROUTING_RANGE:
return _is_support_surface_candidate(source)
if source_kind == ROUTE_CARRIER_KIND_WIRING_CUT_OUT:
return _is_wiring_cut_out_candidate(source)
return True
def _clear_invalid_source_route_metadata(source):
for property_name in (
"QetRouteCarrierName",
"QetRouteCarrierNamesJson",
"QetRoutingObstacleMode",
):
if property_name not in getattr(source, "PropertiesList", []) and not getattr(source, property_name, ""):
continue
TerminalObjects.ensure_string_property(
source,
property_name,
PROPERTY_GROUP,
"Cleared invalid routing source metadata",
"",
)
def _document_object_by_name(doc, name):
if doc is None or not name:
return None
try:
return doc.getObject(name)
except Exception:
return None
def cleanup_invalid_source_carriers(doc):
"""Remove generated carriers whose FreeCAD source object is missing or invalid."""
if doc is None:
return 0
removed = 0
for carrier in list(collect_route_carriers(doc)):
source_name = (getattr(carrier, "QetRouteSourceName", "") or "").strip()
source_kind = (getattr(carrier, "QetRouteSourceKind", "") or "").strip()
if source_kind not in MANAGED_ROUTE_SOURCE_KINDS or not source_name:
continue
if _document_object_by_name(doc, source_name) is None:
removed += _remove_route_carriers(doc, [carrier])
for source in list(getattr(doc, "Objects", []) or []):
if source is None or is_route_carrier(source):
continue
source_kind = _source_kind_value(source)
if source_kind not in MANAGED_ROUTE_SOURCE_KINDS:
continue
if not _route_source_carrier_names(source):
continue
if _source_is_valid_for_kind(source, source_kind):
continue
removed += _remove_route_carriers(doc, _live_source_carriers(doc, source))
_clear_invalid_source_route_metadata(source)
if removed:
try:
doc.recompute()
except Exception:
pass
return removed
def detect_wire_duct_sources(doc, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT):
"""Return document objects that look like wire ducts based on semantics/name and shape."""
sources = []
@ -1671,6 +1851,7 @@ def prepare_layout_space_sources_from_document(doc, project_uuid=""):
raise RoutingNetworkError("No FreeCAD document is available.")
WiringObjects.ensure_wiring_root_group(doc, project_uuid)
cleanup_invalid_source_carriers(doc)
wire_duct_sources = detect_wire_duct_sources(doc)
support_surface_sources = detect_support_surface_sources(doc)
@ -1712,14 +1893,16 @@ def create_wire_duct_carriers_from_document(
min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT,
):
"""Auto-detect wire duct objects in the document and create WireDuct centerlines."""
cleanup_invalid_source_carriers(doc)
created = []
for index, source in enumerate(detect_wire_duct_sources(doc, min_aspect=min_aspect), start=1):
bbox = _bound_box_from_object(source)
if bbox is None:
continue
source_margin = _wire_duct_end_margin_value(source, default=margin)
spec = _wire_duct_centerline_spec_from_bbox(
bbox,
margin=margin,
margin=source_margin,
min_aspect=min_aspect,
)
points = spec.get("centerline", [])
@ -1733,6 +1916,7 @@ def create_wire_duct_carriers_from_document(
spec,
project_uuid=project_uuid,
capacity=capacity,
end_margin=source_margin,
):
continue
carrier = create_route_carrier(
@ -1758,22 +1942,40 @@ def create_wire_duct_carriers_from_document(
)
source_created.append(open_end_carrier)
created.append(open_end_carrier)
_mark_wire_duct_source(source, carrier, source_created)
_mark_wire_duct_source(source, carrier, source_created, end_margin=source_margin)
return created
def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""):
def create_wiring_cut_out_carriers_from_document(
doc,
project_uuid="",
bridge_extension=DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION,
):
"""Create pass-through route carriers for wiring cut-out objects."""
cleanup_invalid_source_carriers(doc)
created = []
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)
source_bridge_extension = _wiring_cut_out_bridge_extension_value(source, default=bridge_extension)
points = _wiring_cut_out_points_from_bbox(bbox, bridge_extension=source_bridge_extension)
if len(points) < 2:
continue
live_carrier = _live_source_carrier(doc, source)
if live_carrier is not None:
if _update_route_carrier(
live_carrier,
points,
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_WIRING_CUT_OUT,
):
_mark_wiring_cut_out_source(source, live_carrier, bridge_extension=source_bridge_extension)
try:
doc.recompute()
except Exception:
pass
continue
label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wiring Cut-Out"
carrier = create_route_carrier(
doc,
@ -1782,7 +1984,7 @@ def create_wiring_cut_out_carriers_from_document(doc, project_uuid=""):
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_WIRING_CUT_OUT,
)
_mark_wiring_cut_out_source(source, carrier)
_mark_wiring_cut_out_source(source, carrier, bridge_extension=source_bridge_extension)
created.append(carrier)
return created
@ -1795,10 +1997,9 @@ def create_surface_carriers_from_document(
margin=DEFAULT_SURFACE_MARGIN,
):
"""Auto-detect thin support panels and create low-priority RoutingRange grids."""
cleanup_invalid_source_carriers(doc)
created = []
for source in detect_support_surface_sources(doc):
if _live_source_carrier(doc, source) is not None:
continue
bbox = _bound_box_from_object(source)
if bbox is None:
continue
@ -1809,6 +2010,46 @@ def create_surface_carriers_from_document(
offset=offset,
margin=margin,
)
live_carriers = _live_source_carriers(doc, source)
if live_carriers:
updated = []
for carrier, points in zip(live_carriers, grids):
if _update_route_carrier(
carrier,
points,
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_ROUTING_RANGE,
):
updated.append(carrier)
source_created = []
label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Support Surface"
for index, points in enumerate(grids[len(live_carriers):], start=len(live_carriers) + 1):
if len(points) < 2:
continue
carrier = create_route_carrier(
doc,
points,
label="QET Auto Support Surface Route {0} {1}".format(label, index),
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_ROUTING_RANGE,
)
source_created.append(carrier)
created.append(carrier)
for stale_carrier in live_carriers[len(grids):]:
_detach_from_groups(doc, stale_carrier)
try:
if doc.getObject(getattr(stale_carrier, "Name", "")) is not None:
doc.removeObject(stale_carrier.Name)
except Exception:
pass
current_carriers = updated + source_created
if updated:
_mark_support_surface_source(source, current_carriers)
try:
doc.recompute()
except Exception:
pass
continue
source_created = []
label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Support Surface"
for index, points in enumerate(grids, start=1):
@ -1952,6 +2193,7 @@ def create_routing_path_network_from_document(
selection_ex=None,
terminal_exit_length=20.0,
terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE,
adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE,
):
"""Generate the EPLAN-style routing path network for the layout space.
@ -2013,7 +2255,10 @@ def create_routing_path_network_from_document(
"surface_carriers": len(surfaces),
"terminal_access_carriers": len(terminal_access),
"layout_space": layout_space,
"network": network_summary(doc),
"network": network_summary(
doc,
adjoining_duct_tolerance=adjoining_duct_tolerance,
),
}
@ -2025,14 +2270,16 @@ def create_wire_duct_carriers_from_selection(
min_aspect=1.5,
):
"""Create WireDuct centerline carriers from selected duct-like solids."""
cleanup_invalid_source_carriers(doc)
created = []
for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1):
bbox = _bound_box_from_object(source)
if bbox is None:
continue
source_margin = _wire_duct_end_margin_value(source, default=margin)
spec = _wire_duct_centerline_spec_from_bbox(
bbox,
margin=margin,
margin=source_margin,
min_aspect=min_aspect,
)
points = spec.get("centerline", [])
@ -2046,6 +2293,7 @@ def create_wire_duct_carriers_from_selection(
spec,
project_uuid=project_uuid,
capacity=capacity,
end_margin=source_margin,
):
continue
carrier = create_route_carrier(
@ -2071,7 +2319,7 @@ def create_wire_duct_carriers_from_selection(
)
source_created.append(open_end_carrier)
created.append(open_end_carrier)
_mark_wire_duct_source(source, carrier, source_created)
_mark_wire_duct_source(source, carrier, source_created, end_margin=source_margin)
return created
@ -2426,11 +2674,22 @@ def connect_point_to_network(network, point):
def _carrier_track_payload(carrier):
return {
payload = {
"name": getattr(carrier, "Name", ""),
"label": getattr(carrier, "Label", ""),
"kind": (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND,
"capacity": _route_carrier_capacity_value(carrier, default=1),
}
source_fields = (
("source_name", "QetRouteSourceName"),
("source_label", "QetRouteSourceLabel"),
("source_kind", "QetRouteSourceKind"),
)
for payload_key, property_name in source_fields:
value = (getattr(carrier, property_name, "") or "").strip()
if value:
payload[payload_key] = value
return payload
def _segment_usage_key(carrier, from_key, to_key):
@ -2566,8 +2825,8 @@ def path_points(network, path_keys):
return [nodes[key] for key in path_keys or [] if key in nodes]
def network_summary(doc):
network = build_route_graph(doc)
def network_summary(doc, adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE):
network = build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance)
return _network_summary_from_graph(network)
@ -2673,12 +2932,13 @@ def diagnose_routing_path_network(
doc,
terminal_exit_length=20.0,
terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE,
adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE,
):
"""Inspect the generated routing path network without routing wires."""
if doc is None:
raise RoutingNetworkError("No FreeCAD document is available.")
network = build_route_graph(doc)
network = build_route_graph(doc, adjoining_duct_tolerance=adjoining_duct_tolerance)
components = _route_graph_components(network)
summary = _network_summary_from_graph(network)
isolated_components = components if len(components) > 1 else []
@ -2700,6 +2960,8 @@ def diagnose_routing_path_network(
"access_carrier": getattr(access_carrier, "Name", "") if access_carrier is not None else "",
"nearest_network_distance_mm": None if distance is None else float(distance),
"nearest_network_point": None if nearest_point is None else _point_payload(nearest_point),
"terminal_access_max_distance_mm": float(max_distance),
"terminal_exit_length_mm": float(max(float(terminal_exit_length or 0.0), 0.0)),
"code": "terminal_access_missing" if not access_live else "terminal_access_too_far",
}
)
@ -2799,11 +3061,13 @@ def write_routing_path_network_diagnostic(
project_uuid="",
terminal_exit_length=20.0,
terminal_access_max_distance=DEFAULT_TERMINAL_ACCESS_MAX_DISTANCE,
adjoining_duct_tolerance=DEFAULT_ADJOINING_DUCT_TOLERANCE,
):
diagnostic = diagnose_routing_path_network(
doc,
terminal_exit_length=terminal_exit_length,
terminal_access_max_distance=terminal_access_max_distance,
adjoining_duct_tolerance=adjoining_duct_tolerance,
)
group = WiringObjects.ensure_diagnostic_group(doc, project_uuid)
_clear_routing_path_network_diagnostics(doc, group)

@ -281,6 +281,55 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("WireDuct", payload["route_track"]["segments"][0]["carrier"]["kind"])
self.assertTrue(json.loads(wire.QetRouteTrackJson)["carrier_names"])
def test_route_track_preserves_generated_carrier_source_metadata(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0))
duct = doc.addObject("Part::Feature", "WireDuctA")
duct.Label = "线槽A"
duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25))
auto_routing_panel.AutoRoutingController().generate_routing_paths()
result = auto_routing.route_eplan_connection_between_terminals(doc, start, end)
route_track = json.loads(result["wire"].QetRouteTrackJson)
wire_duct_carriers = [
segment["carrier"]
for segment in route_track["segments"]
if segment["carrier"]["kind"] == "WireDuct"
]
self.assertTrue(wire_duct_carriers)
self.assertEqual("WireDuctA", wire_duct_carriers[0].get("source_name"))
self.assertEqual("线槽A", wire_duct_carriers[0].get("source_label"))
self.assertEqual("WireDuct", wire_duct_carriers[0].get("source_kind"))
def test_route_track_records_carrier_capacity(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
capacity=3,
)
result = auto_routing.route_eplan_connection_between_terminals(doc, start, end)
route_track = json.loads(result["wire"].QetRouteTrackJson)
self.assertEqual(3, route_track["segments"][0]["carrier"]["capacity"])
def test_network_eplan_connection_route_offsets_lane_by_route_index(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
@ -715,6 +764,37 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("network-dijkstra-v1", result["algorithm"])
self.assertEqual("Routed", result["route_status"])
def test_auto_routing_respects_adjoining_duct_tolerance_option(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(44, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(56, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
result = auto_routing.route_eplan_connection_between_terminals(
doc,
start,
end,
options={"adjoining_duct_tolerance": 15.0},
)
self.assertEqual("Routed", result["route_status"])
self.assertEqual(1, result["network"]["bridged_segments"])
def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
@ -851,6 +931,151 @@ class AutoRoutingTest(unittest.TestCase):
self.assertFalse(hasattr(cabinet, "QetRoutingSourceKind"))
self.assertFalse(hasattr(duct, "QetRoutingSourceKind"))
def test_auto_detect_support_surface_refreshes_routing_range_geometry(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
panel.Label = "安装板A"
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100))
created = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
panel.Shape = FakeShape(FakeBoundBox(20, 140, 0, 5, 0, 100))
created_again = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
carriers = routing_network.collect_route_carriers(doc)
x_values = [
point.x
for carrier in carriers
if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange"
for point in carrier.Points
]
self.assertEqual(6, len(created))
self.assertEqual(0, len(created_again))
self.assertEqual(6, len([carrier for carrier in carriers if carrier.QetRouteCarrierKind == "RoutingRange"]))
self.assertEqual(20.0, min(x_values))
self.assertEqual(140.0, max(x_values))
def test_auto_detect_support_surface_adds_missing_routing_range_lanes_after_resize(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
panel.Label = "安装板A"
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100))
created = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
panel.Shape = FakeShape(FakeBoundBox(0, 180, 0, 5, 0, 120))
created_again = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange"
]
x_values = [point.x for carrier in carriers for point in carrier.Points]
z_values = [point.z for carrier in carriers for point in carrier.Points]
self.assertEqual(6, len(created))
self.assertEqual(1, len(created_again))
self.assertEqual(7, len(carriers))
self.assertEqual(180.0, max(x_values))
self.assertEqual(120.0, max(z_values))
def test_auto_detect_support_surface_removes_stale_routing_range_lanes_after_resize(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
panel.Label = "安装板A"
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100))
created = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
panel.Shape = FakeShape(FakeBoundBox(0, 60, 0, 5, 0, 60))
created_again = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange"
]
x_values = [point.x for carrier in carriers for point in carrier.Points]
z_values = [point.z for carrier in carriers for point in carrier.Points]
self.assertEqual(6, len(created))
self.assertEqual(0, len(created_again))
self.assertEqual(4, len(carriers))
self.assertEqual(60.0, max(x_values))
self.assertEqual(60.0, max(z_values))
def test_auto_detect_support_surface_removes_carriers_and_obstacle_mode_when_source_invalid(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
panel.Label = "安装板A"
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100))
created = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 120, 0, 120))
created_again = routing_network.create_surface_carriers_from_document(
doc,
project_uuid="project-1",
spacing=60.0,
offset=5.0,
margin=0.0,
)
self.assertEqual(6, len(created))
self.assertEqual(0, len(created_again))
self.assertEqual([], routing_network.collect_route_carriers(doc))
self.assertEqual("", getattr(panel, "QetRoutingObstacleMode", ""))
self.assertEqual("", getattr(panel, "QetRouteCarrierNamesJson", ""))
def test_eplan_connection_route_can_use_auto_detected_support_surface(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
@ -979,6 +1204,30 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual([(20.0, 0.0, 10.0), (200.0, 0.0, 10.0)], [(p.x, p.y, p.z) for p in main.Points])
self.assertEqual([20.0, 20.0, 200.0, 200.0], open_end_x_values)
def test_generate_routing_paths_removes_generated_wire_duct_carriers_after_source_deleted(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
duct = doc.addObject("Part::Feature", "WireDuctA")
duct.Label = "Wire Duct A"
duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20))
auto_routing_panel.AutoRoutingController().generate_routing_paths()
generated = [
item
for item in routing_network.collect_route_carriers(doc)
if getattr(item, "QetRouteSourceName", "") == "WireDuctA"
]
doc.removeObject("WireDuctA")
auto_routing_panel.AutoRoutingController().generate_routing_paths()
self.assertEqual(3, len(generated))
self.assertEqual([], routing_network.collect_route_carriers(doc))
def test_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()
@ -1102,59 +1351,145 @@ class AutoRoutingTest(unittest.TestCase):
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):
def test_generate_routing_path_network_refreshes_wiring_cut_out_geometry(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_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",
)
cut_out = doc.addObject("Part::Feature", "WiringCutoutA")
cut_out.Label = "Wiring Cut-Out A"
cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25))
result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1")
diagnostic_group = doc.getObject("QETWiring_05_Diagnostics")
payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson)
first = auto_routing_panel.AutoRoutingController().generate_routing_paths()
cut_out.Shape = FakeShape(FakeBoundBox(65, 75, -2, 2, 15, 25))
second = auto_routing_panel.AutoRoutingController().generate_routing_paths()
cut_out_carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut"
]
self.assertFalse(result["ok"])
self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind)
self.assertEqual(1, len(payload["unconnected_terminals"]))
self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"])
message = auto_routing.format_routing_path_network_report(result["diagnostic"])
self.assertIn("端子未接入", message)
self.assertIn("terminal-far", message)
self.assertIn("4900.0 mm", message)
self.assertIn("补一段线槽/辅助路径", message)
self.assertEqual(1, first["wiring_cut_out_carriers"])
self.assertEqual(0, second["wiring_cut_out_carriers"])
self.assertEqual(1, len(cut_out_carriers))
self.assertEqual([(70.0, -22.0, 20.0), (70.0, 22.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points])
def test_format_routing_path_network_report_tolerates_malformed_samples(self):
def test_wiring_cut_out_source_bridge_extension_controls_generated_path_length(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
diagnostic = {
"issues": [{"code": "external_issue", "count": 1}],
"unconnected_terminals": ["bad-terminal-sample"],
"possible_breaks": ["bad-break-sample"],
"isolated_components": ["bad-component-sample"],
}
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
cut_out = doc.addObject("Part::Feature", "WiringCutoutA")
cut_out.Label = "过线孔A"
cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25))
cut_out.QetWiringCutOutBridgeExtensionMm = 8.0
message = auto_routing.format_routing_path_network_report(diagnostic)
auto_routing_panel.AutoRoutingController().generate_routing_paths()
cut_out_carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut"
]
self.assertIn("布线路径网络检查发现", message)
self.assertIn("首个问题external_issue", message)
self.assertEqual(1, len(cut_out_carriers))
self.assertIn("QetWiringCutOutBridgeExtensionMm", cut_out.PropertiesList)
self.assertEqual(8.0, cut_out.QetWiringCutOutBridgeExtensionMm)
self.assertEqual([(50.0, -10.0, 20.0), (50.0, 10.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points])
def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self):
def test_wiring_cut_out_bridges_nearby_ducts_on_both_sides_of_panel(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, -20, 0))
end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 20, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
[app.Vector(0, -20, 20), app.Vector(50, -20, 20)],
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(50, 20, 20), app.Vector(100, 20, 20)],
project_uuid="project-1",
kind="WireDuct",
)
cut_out = doc.addObject("Part::Feature", "WiringCutoutA")
cut_out.Label = "过线孔A"
cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25))
auto_routing_panel.AutoRoutingController().generate_routing_paths()
result = auto_routing.route_eplan_connection_between_terminals(doc, start, end)
self.assertEqual("Routed", result["route_status"])
self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"])
self.assertEqual(0, result["collision_count"])
def test_check_routing_path_network_writes_diagnostic_for_unconnected_terminal(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalFar", "terminal-far", app.Vector(5000, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1")
diagnostic_group = doc.getObject("QETWiring_05_Diagnostics")
payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson)
self.assertFalse(result["ok"])
self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind)
self.assertEqual(1, len(payload["unconnected_terminals"]))
self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"])
self.assertEqual(1000.0, payload["unconnected_terminals"][0]["terminal_access_max_distance_mm"])
message = auto_routing.format_routing_path_network_report(result["diagnostic"])
self.assertIn("端子未接入", message)
self.assertIn("terminal-far", message)
self.assertIn("4900.0 mm", message)
self.assertIn("端子接入最大距离 1000.0 mm", message)
self.assertIn("补一段线槽/辅助路径", message)
def test_format_routing_path_network_report_tolerates_malformed_samples(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
diagnostic = {
"issues": [{"code": "external_issue", "count": 1}],
"unconnected_terminals": ["bad-terminal-sample"],
"possible_breaks": ["bad-break-sample"],
"isolated_components": ["bad-component-sample"],
}
message = auto_routing.format_routing_path_network_report(diagnostic)
self.assertIn("布线路径网络检查发现", message)
self.assertIn("首个问题external_issue", message)
def test_format_routing_path_network_report_calls_out_wire_duct_break_point(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
label="线槽A",
project_uuid="project-1",
kind="WireDuct",
@ -1168,6 +1503,58 @@ class AutoRoutingTest(unittest.TestCase):
self.assertIn("(0.0, 0.0, 20.0)", message)
self.assertIn("补齐相邻线槽", message)
def test_format_routing_path_network_report_includes_bridged_segment_count(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
diagnostic = {
"summary": {
"carriers": 5,
"segments": 6,
"nodes": 5,
"bridged_segments": 1,
},
"issues": [],
"ok": True,
}
message = auto_routing.format_routing_path_network_report(diagnostic)
self.assertIn("桥接 1 段", message)
def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
for index, points in enumerate(
(
[app.Vector(0, 0, 20), app.Vector(44, 0, 20)],
[app.Vector(56, 0, 20), app.Vector(100, 0, 20)],
[app.Vector(100, 0, 20), app.Vector(100, 100, 20)],
[app.Vector(100, 100, 20), app.Vector(0, 100, 20)],
[app.Vector(0, 100, 20), app.Vector(0, 0, 20)],
),
start=1,
):
routing_network.create_route_carrier(
doc,
points,
label="线槽{0}".format(index),
project_uuid="project-1",
kind="WireDuct",
)
result = auto_routing.check_eplan_routing_path_network(
doc,
project_uuid="project-1",
options={"adjoining_duct_tolerance": 15.0},
)
self.assertTrue(result["ok"])
self.assertEqual(1, result["diagnostic"]["summary"]["bridged_segments"])
self.assertEqual([], result["diagnostic"]["possible_breaks"])
def test_generate_routing_path_network_skips_far_terminal_access_to_protect_view_bbox(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()
@ -1187,6 +1574,55 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual(2, result["wire_duct_open_end_carriers"])
self.assertEqual(0, result["terminal_access_carriers"])
def test_auto_routing_controller_exposes_terminal_access_max_distance(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
duct = doc.addObject("Part::Feature", "WireDuctFar")
duct.Label = "Wire Duct Far"
duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25))
controller = auto_routing_panel.AutoRoutingController()
controller.set_terminal_access_max_distance(6000.0)
result = controller.generate_routing_paths()
self.assertEqual(1, result["terminal_access_carriers"])
self.assertEqual(6000.0, controller.routing_options()["terminal_access_max_distance"])
def test_auto_routing_controller_exposes_terminal_exit_length(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0))
duct = doc.addObject("Part::Feature", "WireDuctA")
duct.Label = "Wire Duct A"
duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25))
controller = auto_routing_panel.AutoRoutingController()
controller.set_terminal_exit_length(40.0)
controller.generate_routing_paths()
access_carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess"
]
self.assertEqual(1, len(access_carriers))
self.assertEqual(
(50.0, 0.0, 40.0),
tuple(getattr(access_carriers[0].Points[0], axis) for axis in ("x", "y", "z")),
)
self.assertEqual(40.0, controller.routing_options()["terminal_exit_length"])
def test_route_eplan_connections_prepares_layout_space_like_eplan_route(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()
@ -1226,6 +1662,158 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual(1, diagnostic_payload["prepared_layout"]["wire_duct_carriers"])
self.assertEqual(2, diagnostic_payload["prepared_layout"]["terminal_access_carriers"])
def test_auto_routing_controller_passes_adjoining_duct_tolerance_to_batch_route(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(44, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(56, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
app._qet_exchange_payload = {
"project_uuid": "project-1",
"wires": [
{
"wire_id": "wire-1",
"start_terminal_uuid": "terminal-start",
"end_terminal_uuid": "terminal-end",
}
],
}
report = auto_routing_panel.AutoRoutingController(
options={"adjoining_duct_tolerance": 15.0}
).route_eplan_connections()
self.assertEqual(1, report["routed"])
self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"])
def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(44, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(56, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
summary = auto_routing_panel.AutoRoutingController(
options={"adjoining_duct_tolerance": 15.0}
).summary()
self.assertIn("桥接1", summary)
def test_auto_routing_controller_exposes_lane_spacing(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0))
_terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
app._qet_exchange_payload = {
"project_uuid": "project-1",
"wires": [
{
"wire_id": "wire-a",
"start_terminal_uuid": "terminal-start-a",
"end_terminal_uuid": "terminal-end-a",
},
{
"wire_id": "wire-b",
"start_terminal_uuid": "terminal-start-b",
"end_terminal_uuid": "terminal-end-b",
},
],
}
controller = auto_routing_panel.AutoRoutingController()
controller.set_lane_spacing(14.0)
report = controller.route_eplan_connections()
self.assertEqual(14.0, controller.routing_options()["lane_spacing"])
self.assertEqual(14.0, report["routes"][1]["lane"]["spacing_mm"])
self.assertEqual(14.0, report["routes"][1]["lane"]["offset_mm"])
def test_auto_routing_controller_exposes_lane_axis(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
_terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0))
_terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0))
_terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(0, 100, 20)],
project_uuid="project-1",
kind="WireDuct",
)
app._qet_exchange_payload = {
"project_uuid": "project-1",
"wires": [
{
"wire_id": "wire-a",
"start_terminal_uuid": "terminal-start-a",
"end_terminal_uuid": "terminal-end-a",
},
{
"wire_id": "wire-b",
"start_terminal_uuid": "terminal-start-b",
"end_terminal_uuid": "terminal-end-b",
},
],
}
controller = auto_routing_panel.AutoRoutingController()
controller.set_lane_spacing(8.0)
controller.set_lane_axis("z")
report = controller.route_eplan_connections()
self.assertEqual("z", controller.routing_options()["lane_axis"])
self.assertEqual("z", report["routes"][1]["lane"]["axis"])
self.assertEqual(8.0, report["routes"][1]["lane"]["offset_mm"])
def test_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()
@ -1367,6 +1955,26 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("PassThrough", duct.QetRoutingObstacleMode)
self.assertEqual([(20.0, 0.0, 15.0), (100.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points])
def test_wire_duct_source_end_margin_controls_generated_centerline_length(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
duct = doc.addObject("Part::Feature", "WireDuctA")
duct.Label = "线槽A"
duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25))
duct.QetWireDuctEndMarginMm = 5.0
created = routing_network.create_wire_duct_carriers_from_document(
doc,
project_uuid="project-1",
)
carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0]
self.assertIn("QetWireDuctEndMarginMm", duct.PropertiesList)
self.assertEqual(5.0, duct.QetWireDuctEndMarginMm)
self.assertEqual([(5.0, 0.0, 15.0), (115.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points])
def test_wire_duct_source_capacity_is_copied_to_generated_carriers(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
@ -1660,6 +2268,125 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"])
self.assertIn("总长度", message)
def test_route_report_includes_route_source_sample_when_available(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
report = {
"routed": 1,
"collision_warnings": 0,
"skipped_missing_terminal": 0,
"routes": [
{
"wire_label": "N4111",
"route_track": {
"segments": [
{"carrier": {"kind": "TerminalAccess", "source_label": "QF1:A1"}},
{"carrier": {"kind": "WireDuct", "source_label": "线槽A"}},
{"carrier": {"kind": "WiringCutOut", "source_label": "过线孔A"}},
{"carrier": {"kind": "WireDuct", "source_label": "线槽A"}},
]
},
}
],
}
message = auto_routing.format_eplan_connection_route_report(report)
self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message)
def test_route_report_includes_network_bridge_and_blocked_segment_counts(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
report = {
"routed": 1,
"collision_warnings": 0,
"skipped_missing_terminal": 0,
"routes": [
{
"network": {
"bridged_segments": 1,
"blocked_segments": 2,
},
}
],
}
message = auto_routing.format_eplan_connection_route_report(report)
self.assertIn("路径网络:自动桥接 1 段相邻线槽,避障屏蔽 2 段。", message)
def test_route_report_includes_parallel_lane_summary(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
report = {
"routed": 2,
"collision_warnings": 0,
"skipped_missing_terminal": 0,
"routes": [
{"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "offset_mm": 0.0}},
{"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "offset_mm": -10.0}},
],
}
message = auto_routing.format_eplan_connection_route_report(report)
self.assertIn("并行错位:最大 lane 2间距 10.0 mm。", message)
def test_route_report_warns_when_parallel_lanes_exceed_track_capacity(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
report = {
"routed": 3,
"collision_warnings": 0,
"skipped_missing_terminal": 0,
"routes": [
{
"lane": {"index": 2, "spacing_mm": 10.0},
"route_track": {
"segments": [
{"carrier": {"kind": "WireDuct", "capacity": 2}},
{"carrier": {"kind": "WireDuct", "capacity": 4}},
]
},
}
],
}
message = auto_routing.format_eplan_connection_route_report(report)
self.assertIn("容量提示:最大并行线数 3路径最小容量 2。", message)
def test_route_report_capacity_pressure_is_checked_per_route(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()
report = {
"routed": 2,
"collision_warnings": 0,
"skipped_missing_terminal": 0,
"routes": [
{
"lane": {"index": 2, "spacing_mm": 10.0},
"route_track": {
"segments": [
{"carrier": {"kind": "WireDuct", "capacity": 4}},
]
},
},
{
"lane": {"index": 0, "spacing_mm": 10.0},
"route_track": {
"segments": [
{"carrier": {"kind": "WireDuct", "capacity": 1}},
]
},
},
],
}
message = auto_routing.format_eplan_connection_route_report(report)
self.assertNotIn("容量提示", message)
def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()

Loading…
Cancel
Save