feat(freecad): 支持SW类布线路径草图

dev
Zhaowenlong 2 weeks ago
parent 3615f62442
commit d1c7169cad

@ -1040,6 +1040,35 @@ SW 教程中路径可以由“创建草图”或“转换草图”得到,并
如果临时画的 Draft 线或草图线明显悬空,可同时选中安装板/柜板上的支撑面 Face 和该路径对象,再执行“选中路径作为用户路径”或“生成布线路径网络”。系统会把路径点投影到支撑面附近,而不是直接使用 Draft 当前工作平面坐标。 如果临时画的 Draft 线或草图线明显悬空,可同时选中安装板/柜板上的支撑面 Face 和该路径对象,再执行“选中路径作为用户路径”或“生成布线路径网络”。系统会把路径点投影到支撑面附近,而不是直接使用 Draft 当前工作平面坐标。
#### 6.4.1 SW 类布线路径草图 MVP
为贴近 SW Electrical 视频中的“选面/离面画路径,再生成 3D 布线”的操作,当前 `3D 布线连接` 面板提供了轻量的布线路径草图入口。它不是完整复刻 SW Sketcher而是面向电气布线的 `UserPath` 绘制子集:
1. `草图离面距离 mm`:创建草图时沿选中 Face 法向偏移指定距离。设为 0 时贴近选中面;设为 30、50 等数值时,可在离安装板或柜门一定距离的位置画路径。
2. `创建布线路径草图`:先选中机柜面板、安装板、门板或线槽上的一个 Face再点击该按钮。系统会创建黄色 `布线路径草图`,写入 `AttachmentSupport`、`MapMode=FlatFace`、`AttachmentOffset` 和 `QetRouteSketchMode=ManualUserPathSketch`,并尽量进入 Sketcher 编辑。
3. 用户在 Sketcher 中用普通线段、折线、水平/垂直/重合等约束画路径。Sketcher 的水平/垂直是草图本地平面方向;如果要画全局 Z 向路径,应选竖直 Face 建草图,或使用正交 3D 路径按钮。
4. 退出草图后,直接点击 `生成布线路径网络` 即可。系统会自动扫描明确标记为布线路径的草图并生成/刷新 `UserPath` carrier不要求再手动点一次 `选中路径作为用户路径`
5. 为避免误把设备或柜体里的普通建模草图当成布线路径,文档级自动扫描只自动纳入明确标记/命名的路径源,例如 `QetRoutingSourceKind=UserPath`、`ManualUserPathSketch`、名称或标签包含 `UserPath`、`Route Path`、`布线路径`、`走线路径`、`用户路径` 等。普通 `Sketch001` 不会自动转换;如果确实要把普通草图作为路径,仍可选中它后点击 `选中路径作为用户路径`
6. 明确标记为 `UserPath``ManualUserPathSketch` 的草图优先按布线路径处理,即使名称里同时包含“安装板”“门板”等支撑面关键词,也不会被误跳过。这样现场可以使用“安装板布线路径草图”这类更自然的命名。
7. 草图修改后重新生成布线路径网络会刷新同源 `UserPath`。如果把草图里的线段全部删除,系统会同步删除之前由该草图生成的旧 carrier并清空源草图上的 carrier 名称记录,避免旧路径继续参与自动布线。
对于跨平面或竖向过渡,面板还提供两种 3D 点选路径:
1. `选中点生成3D路径`:按选择顺序把多个 3D 点、顶点、边端点或带 `Points` 的对象串成一条自由空间 `UserPath`。适合现场补一段明确的空间连接,但可能生成斜线。
2. `选中点生成正交3D路径`:按选择顺序生成只沿 X/Y/Z 单轴变化的折线路径,并标记 `QetRoutePathMode=Orthogonal3D`。它更接近 SW 3D 草图里锁定 X/Y/Z 方向画路径的效果,也更适合减少斜穿设备或柜体。
推荐手测顺序:
```text
1. 选中安装板/柜门/线槽 Face。
2. 设置“草图离面距离 mm”。
3. 点击“创建布线路径草图”。
4. 在 Sketcher 里画黄色主路径,退出草图。
5. 如有跨面路径,按顺序选择端点/顶点点击“选中点生成正交3D路径”。
6. 点击“生成布线路径网络”。
7. 点击“检查布线路径网络”和“生成布线连接”。
```
`UserPath` 与线槽的关系: `UserPath` 与线槽的关系:
```text ```text
@ -1129,7 +1158,9 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points)
## 7. 当前限制 ## 7. 当前限制
当前版本可完成布线连接原型,但仍有以下限制: 当前版本已进入第一阶段收尾:可从 QET 导线任务生成 FreeCAD 侧布线路径网络和 `AutoSuggested` 导线;可识别/生成 `WireDuct`、`UserPath`、`WiringCutOut`、`RoutingRange`、`TerminalAccess`;可通过“创建布线路径草图”“选中点生成正交 3D 路径”等入口完成接近 SW 的电气布线路径草图/UserPath 绘制 MVP。这个 MVP 已覆盖选 Face 创建离面草图、Sketcher 画线、生成网络自动转 `UserPath`、同源刷新、清空草图后删除旧 carrier、普通机械草图不误转换、真实 Sketcher 坐标不重复应用 Placement 等关键手测场景。
但第一阶段仍不是 SW/EPLAN 完整布线系统,主要限制如下:
1. 线槽实体中心线生成基于包围盒长轴,不理解真实线槽开口、盖板和内部空间。 1. 线槽实体中心线生成基于包围盒长轴,不理解真实线槽开口、盖板和内部空间。
2. 多根线共路时已做基础错位显示,但不是线束级排布,也不计算每根线在线槽内的真实截面位置。 2. 多根线共路时已做基础错位显示,但不是线束级排布,也不计算每根线在线槽内的真实截面位置。
@ -1141,6 +1172,7 @@ TemplateAuthoring.set_template_terminal_local_route_points(terminal, points)
8. 端子出线方向默认依赖端子 LCS 方向;若工程端子提供 `QetTerminalLocalRoutePointsJson`,会优先使用局部路径。模板端子方向或局部路径不准时,布线连接会受影响。 8. 端子出线方向默认依赖端子 LCS 方向;若工程端子提供 `QetTerminalLocalRoutePointsJson`,会优先使用局部路径。模板端子方向或局部路径不准时,布线连接会受影响。
9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。 9. 导线几何当前保存在 FreeCAD 文档,不作为第一版数据库字段回写。
10. 当前不自动求解导轨、安装板和设备之间的 Assembly 配合关系;装配位置以 `scene.FCStd` 中对象的最终 `Placement` 为准。 10. 当前不自动求解导轨、安装板和设备之间的 Assembly 配合关系;装配位置以 `scene.FCStd` 中对象的最终 `Placement` 为准。
11. `创建布线路径草图` 只提供面向电气路径的轻量草图入口,不复刻 SW 3D Sketch 的全部约束、捕捉、智能标注和工程规则跨平面路径当前优先用“选中点生成正交3D路径”补充。
## 8. 后续需要完成 ## 8. 后续需要完成

@ -976,15 +976,63 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。
1. 面板状态摘要显示 `版本2026-06-08-runtime-routing-v4` 或更新版本。 1. 面板状态摘要显示 `版本2026-06-08-runtime-routing-v4` 或更新版本。
2. `检查布线准备度` 能识别到 QET 导线来源、工程端子、路径网络和柜内边界;若有问题,`RoutingPreflight.QetDiagnosticIssueCodes` 能说明原因。 2. `检查布线准备度` 能识别到 QET 导线来源、工程端子、路径网络和柜内边界;若有问题,`RoutingPreflight.QetDiagnosticIssueCodes` 能说明原因。
3. `检查布线路径网络` 不应出现空网络、路径对象几何无效、端子越出柜内边界或路径越出柜内边界;如果出现,应先修装配或路径源。 3. `检查布线路径网络` 不应出现空网络、路径对象几何无效、端子越出柜内边界或路径越出柜内边界;如果出现,应先修装配或路径源。
4. `生成布线连接` 后,`RoutingConnectionBatch.QetDiagnosticJson.runtime_version` 与面板版本一致。 4. 如果使用 `创建布线路径草图`,退出 Sketcher 后点击 `生成布线路径网络`,应能在树目录看到对应 `UserPath` carrier源草图应保留 `QetRouteSketchMode=ManualUserPathSketch`,生成的 carrier 应保留 `QetRouteSourceLabel` / `QetRouteSourceName`
5. 有导线任务时,`RoutingConnectionBatch.routed` 应大于 0如果为 0应优先看 `missing_endpoint_samples`、`missing_route_network_samples` 或 `error_samples` 5. 如果在竖直 Face 上创建草图,生成的 `UserPath` 点应沿该 Face 的离面方向偏移,不应出现离面距离翻倍或路径跑到柜外;若出现,优先记录源草图 Label、离面距离和生成 carrier 的 `Points`
6. 正常导线的单线 `QetRouteIssueCodes` 应为空;若存在问题码,应能归类到端子接入、碰撞、柜内越界、容量压力、路径质量或路径约束。 6. 如果把布线路径草图里的线段全部删掉,再点击 `生成布线路径网络`,之前由该草图生成的旧 `UserPath` 应被清理,不应继续参与布线。
7. 已标记 `CabinetInterior` 时,优先要求 `QetRouteBoundaryStatus=InsideBoundary`;出现 `BoundaryWarning` 时,应补柜内主路径或修正边界。 7. `生成布线连接` 后,`RoutingConnectionBatch.QetDiagnosticJson.runtime_version` 与面板版本一致。
8. 碰撞状态优先看 `QetRouteCollisionStatus``NoCollision` 为理想结果,`ClearanceWarning` 可作为间隙问题记录,`HardIntersectionWarning` 视为穿模问题。 8. 有导线任务时,`RoutingConnectionBatch.routed` 应大于 0如果为 0应优先看 `missing_endpoint_samples`、`missing_route_network_samples` 或 `error_samples`
9. 多根线共路时,检查 `QetRouteLaneIndex`、`QetRouteLaneOffsetMm` 和 `QetRouteCapacityStatus`,确认新增导线不会无诊断地贴到旧线上。 9. 正常导线的单线 `QetRouteIssueCodes` 应为空;若存在问题码,应能归类到端子接入、碰撞、柜内越界、容量压力、路径质量或路径约束。
10. 最后点击 `汇总布线诊断`,把 `RoutingDiagnosticSummary.QetDiagnosticMessage` 和主要 `issue_codes` 作为本次手测结论。 10. 已标记 `CabinetInterior` 时,优先要求 `QetRouteBoundaryStatus=InsideBoundary`;出现 `BoundaryWarning` 时,应补柜内主路径或修正边界。
11. 碰撞状态优先看 `QetRouteCollisionStatus``NoCollision` 为理想结果,`ClearanceWarning` 可作为间隙问题记录,`HardIntersectionWarning` 视为穿模问题。
12. 多根线共路时,检查 `QetRouteLaneIndex`、`QetRouteLaneOffsetMm` 和 `QetRouteCapacityStatus`,确认新增导线不会无诊断地贴到旧线上。
13. 最后点击 `汇总布线诊断`,把 `RoutingDiagnosticSummary.QetDiagnosticMessage` 和主要 `issue_codes` 作为本次手测结论。
如果上面 1、4、5 成立,且问题导线都能通过诊断字段定位原因,说明当前版本已经具备第一版可测闭环。若仍存在导线穿模、柜外线或未布通,应优先把对应导线的 `wire_object_label`、`QetRouteIssueCodes`、`QetRouteDiagnosticsJson` 和录屏时间点一起反馈。 如果上面 1、4、7、8 成立,且问题导线都能通过诊断字段定位原因,说明当前版本已经具备第一版可测闭环。若仍存在导线穿模、柜外线或未布通,应优先把对应导线的 `wire_object_label`、`QetRouteIssueCodes`、`QetRouteDiagnosticsJson` 和录屏时间点一起反馈。
### 14.3 手测反馈记录模板
建议每次 GUI 手测后按下面格式记录,便于开发侧快速判断问题来自装配、路径网络、端子接入还是导线求路:
```text
测试工程:
FreeCAD 版本/运行目录:
面板 runtime_version
是否从 QET 3D 按钮打开:
是否重新生成工程端子:
是否标记 CabinetInterior
路径输入:
- 线槽数量/是否标记为线槽:
- UserPath 来源:创建布线路径草图 / 选中路径作为用户路径 / 选中点生成正交3D路径 / 诊断桥接
- 是否使用竖直 Face 创建草图:
- 草图离面距离 mm
- 生成布线路径网络后 UserPath carrier 数量:
批量布线结果:
- total_wires
- routed
- missing_terminals
- missing_route_network
- collision_warnings
- boundary warnings
- main_path_not_used / fallback_routes
汇总诊断:
- RoutingDiagnosticSummary.QetDiagnosticMessage
- 主要 issue_codes
- 下一步建议动作:
异常样例:
- 录屏时间点:
- wire_object_label
- QetRouteIssueCodes
- QetRouteSourceLabels / QetRouteCarrierNames
- QetRouteAccessStatus
- QetRouteCollisionStatus
- QetRouteBoundaryStatus
- QetRouteQualityStatus
- 简要现象:未布通 / 穿模 / 跑出柜外 / 接入过长 / 未走主路径 / 线样式不对
```
--- ---
@ -1040,13 +1088,23 @@ FreeCADExchange 会生成 3D -> 2D 的回写结果。
1. 先把导轨、线槽和设备摆到机柜内真实位置。 1. 先把导轨、线槽和设备摆到机柜内真实位置。
2. 选择线槽,点击 `标记为线槽` 2. 选择线槽,点击 `标记为线槽`
3. 如果没有线槽,先画草图或 Draft 线,再点击 `选中路径作为用户路径` 3. 如果没有线槽,或需要补柜内主路径,优先使用 `3D布线连接` 面板的 SW 类路径流程:
4. 打开 `3D布线连接` - 选中安装板、柜门、线槽或柜板上的一个 Face。
5. 点击 `检查布线准备度`,确认有布线源。 - 设置 `草图离面距离 mm`,例如 0、20、50。
6. 点击 `准备布线布局空间` - 点击 `创建布线路径草图`
7. 点击 `生成布线路径网络` - 在 Sketcher 中画横竖路径线,退出草图。
8. 点击 `检查布线路径网络` - 点击 `生成布线路径网络`,系统会自动把明确标记的布线路径草图转换为 `UserPath`
9. 再尝试生成布线连接。 - 如果要画全局 Z 向或竖向过渡路径,优先选柜侧板、门板内侧、线槽侧面等竖直 Face 创建草图Sketcher 里的水平/垂直约束始终相对当前草图平面。
4. 如果需要跨平面或 Z 向过渡路径,按顺序选择多个 3D 点、顶点或边端点,再点击 `选中点生成正交3D路径`。该路径会按 X/Y/Z 折线生成,适合补线槽之间、门板到柜内、设备区域到主路径之间的连接。
5. 如果已经有普通草图或 Draft 线,也可以选中它后点击 `选中路径作为用户路径`。普通 `Sketch001` 这类未标记机械草图不会被 `生成布线路径网络` 自动当成布线路径,避免误转换。
- 通过 `创建布线路径草图` 生成的草图已经写入布线路径标记,可以命名为“安装板布线路径草图”“门板布线路径草图”等;即使名称里带安装板、门板这类支撑面词,也会按 `UserPath` 处理。
- 如果回到草图里把路径线全部删掉,再点击 `生成布线路径网络` 会清理之前生成的旧 `UserPath`,不用手动删除树目录里的旧 carrier。
6. 打开 `3D布线连接`
7. 点击 `检查布线准备度`,确认有布线源。
8. 点击 `准备布线布局空间`
9. 点击 `生成布线路径网络`
10. 点击 `检查布线路径网络`
11. 再尝试生成布线连接。
### 15.4 为什么保存后 QET 看不到 3D 位姿? ### 15.4 为什么保存后 QET 看不到 3D 位姿?

@ -101,6 +101,7 @@ class AutoRoutingController:
def __init__(self, options=None): def __init__(self, options=None):
self.last_report = None self.last_report = None
self.options = dict(options or {}) self.options = dict(options or {})
self.options.setdefault("user_path_sketch_offset", RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET)
def routing_options(self): def routing_options(self):
return dict(self.options) return dict(self.options)
@ -174,6 +175,13 @@ class AutoRoutingController:
capacity = 1 capacity = 1
self.options["selected_route_capacity"] = max(capacity, 1) self.options["selected_route_capacity"] = max(capacity, 1)
def set_user_path_sketch_offset(self, value):
try:
offset = float(value)
except Exception:
offset = RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET
self.options["user_path_sketch_offset"] = max(offset, 0.0)
def set_preflight_routeability_sample_limit(self, value): def set_preflight_routeability_sample_limit(self, value):
try: try:
sample_limit = int(value) sample_limit = int(value)
@ -1846,6 +1854,84 @@ class AutoRoutingController:
} }
return self.last_report return self.last_report
def create_user_path_sketch_from_selection(self):
doc = _active_document()
project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip()
result = RoutingNetwork.create_user_path_sketch_from_selection(
doc,
_selection_ex(),
project_uuid=project_uuid,
offset=float(
self.routing_options().get(
"user_path_sketch_offset",
RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET,
)
or 0.0
),
)
sketch = result.get("sketch")
self.last_report = {
"user_path_sketches": 1 if sketch is not None else 0,
"user_path_sketch_names": [getattr(sketch, "Name", "")] if sketch is not None else [],
"support_name": getattr(result.get("support"), "Name", "") if result.get("support") is not None else "",
"support_sub_element": result.get("sub_element_name", ""),
"sketch": sketch,
}
return self.last_report
def create_user_path_from_selected_points(self):
doc = _active_document()
project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip()
result = RoutingNetwork.create_user_path_carrier_from_selected_points(
doc,
_selection_ex(),
project_uuid=project_uuid,
)
carrier = result.get("carrier")
self.last_report = {
"user_path_carriers": 1 if carrier is not None else 0,
"user_path_carrier_names": [getattr(carrier, "Name", "")] if carrier is not None else [],
"user_path_point_count": len(result.get("points", []) or []),
"network": RoutingNetwork.network_summary(
doc,
adjoining_duct_tolerance=float(
self.routing_options().get(
"adjoining_duct_tolerance",
RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE,
)
or 0.0
),
),
}
return self.last_report
def create_orthogonal_user_path_from_selected_points(self):
doc = _active_document()
project_uuid = getattr(TerminalObjects.ensure_root_group(doc), "QetProjectUuid", "").strip()
result = RoutingNetwork.create_orthogonal_user_path_carrier_from_selected_points(
doc,
_selection_ex(),
project_uuid=project_uuid,
)
carrier = result.get("carrier")
self.last_report = {
"user_path_carriers": 1 if carrier is not None else 0,
"user_path_carrier_names": [getattr(carrier, "Name", "")] if carrier is not None else [],
"user_path_point_count": len(result.get("points", []) or []),
"user_path_mode": "Orthogonal3D",
"network": RoutingNetwork.network_summary(
doc,
adjoining_duct_tolerance=float(
self.routing_options().get(
"adjoining_duct_tolerance",
RoutingNetwork.DEFAULT_ADJOINING_DUCT_TOLERANCE,
)
or 0.0
),
),
}
return self.last_report
def set_selected_terminal_local_route_points(self): def set_selected_terminal_local_route_points(self):
result = RoutingNetwork.set_terminal_local_route_points_from_selection(_selection_ex()) result = RoutingNetwork.set_terminal_local_route_points_from_selection(_selection_ex())
terminal = result.get("terminal") terminal = result.get("terminal")
@ -2326,6 +2412,21 @@ class AutoRoutingTaskPanel:
int(self.controller.routing_options().get("selected_route_capacity", 4) or 4) int(self.controller.routing_options().get("selected_route_capacity", 4) or 4)
) )
options_layout.addWidget(self.selected_route_capacity_spin) options_layout.addWidget(self.selected_route_capacity_spin)
options_layout.addWidget(QtWidgets.QLabel("草图离面距离 mm"))
self.user_path_sketch_offset_spin = QtWidgets.QDoubleSpinBox()
self.user_path_sketch_offset_spin.setRange(0.0, 10000.0)
self.user_path_sketch_offset_spin.setDecimals(1)
self.user_path_sketch_offset_spin.setSingleStep(5.0)
self.user_path_sketch_offset_spin.setValue(
float(
self.controller.routing_options().get(
"user_path_sketch_offset",
RoutingNetwork.DEFAULT_ROUTE_PATH_FACE_OFFSET,
)
or 0.0
)
)
options_layout.addWidget(self.user_path_sketch_offset_spin)
self.generate_layout_button = _style_command_button( self.generate_layout_button = _style_command_button(
QtWidgets.QPushButton(), QtWidgets.QPushButton(),
@ -2339,6 +2440,24 @@ class AutoRoutingTaskPanel:
"按 EPLAN 逻辑从布局空间生成完整 routing path network线槽、布线区域和端子接入。", "按 EPLAN 逻辑从布局空间生成完整 routing path network线槽、布线区域和端子接入。",
) )
self.create_user_path_sketch_button = _style_command_button(
QtWidgets.QPushButton(),
"创建布线路径草图",
"先选中机柜面板、安装板、门板或线槽的一个 Face再创建一次性贴面草图生成布线路径网络时会自动转换为 UserPath。",
)
self.create_3d_user_path_button = _style_command_button(
QtWidgets.QPushButton(),
"选中点生成3D路径",
"按选择顺序把多个 3D 点、顶点、边端点或带 Points 的对象串成一条 UserPath适合补竖向或跨平面路径。",
)
self.create_orthogonal_3d_user_path_button = _style_command_button(
QtWidgets.QPushButton(),
"选中点生成正交3D路径",
"按选择顺序把多个 3D 点串成只沿 X/Y/Z 单轴变化的折线路径,减少斜穿设备和柜体。",
)
self.create_user_paths_button = _style_command_button( self.create_user_paths_button = _style_command_button(
QtWidgets.QPushButton(), QtWidgets.QPushButton(),
"选中路径作为用户路径", "选中路径作为用户路径",
@ -2558,6 +2677,9 @@ class AutoRoutingTaskPanel:
for widget in ( for widget in (
self.generate_layout_button, self.generate_layout_button,
self.create_user_path_sketch_button,
self.create_3d_user_path_button,
self.create_orthogonal_3d_user_path_button,
self.create_user_paths_button, self.create_user_paths_button,
self.set_terminal_local_route_button, self.set_terminal_local_route_button,
self.create_user_path_bridge_button, self.create_user_path_bridge_button,
@ -2604,6 +2726,9 @@ class AutoRoutingTaskPanel:
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
self.generate_paths_button.clicked.connect(self.generate_routing_paths) self.generate_paths_button.clicked.connect(self.generate_routing_paths)
self.create_user_path_sketch_button.clicked.connect(self.create_user_path_sketch_from_selection)
self.create_3d_user_path_button.clicked.connect(self.create_user_path_from_selected_points)
self.create_orthogonal_3d_user_path_button.clicked.connect(self.create_orthogonal_user_path_from_selected_points)
self.create_user_paths_button.clicked.connect(self.create_user_paths_from_selection) self.create_user_paths_button.clicked.connect(self.create_user_paths_from_selection)
self.set_terminal_local_route_button.clicked.connect(self.set_selected_terminal_local_route_points) self.set_terminal_local_route_button.clicked.connect(self.set_selected_terminal_local_route_points)
self.create_user_path_bridge_button.clicked.connect(self.create_user_path_bridge_from_selection) self.create_user_path_bridge_button.clicked.connect(self.create_user_path_bridge_from_selection)
@ -2686,6 +2811,7 @@ class AutoRoutingTaskPanel:
self.controller.set_lane_max_offset(self.lane_max_offset_spin.value()) self.controller.set_lane_max_offset(self.lane_max_offset_spin.value())
self.controller.set_lane_axis(self.lane_axis_combo.currentText()) self.controller.set_lane_axis(self.lane_axis_combo.currentText())
self.controller.set_selected_route_capacity(self.selected_route_capacity_spin.value()) self.controller.set_selected_route_capacity(self.selected_route_capacity_spin.value())
self.controller.set_user_path_sketch_offset(self.user_path_sketch_offset_spin.value())
def generate_routing_paths(self): def generate_routing_paths(self):
try: try:
@ -2715,6 +2841,92 @@ class AutoRoutingTaskPanel:
except Exception as exc: except Exception as exc:
self._set_error(str(exc)) self._set_error(str(exc))
def _select_and_edit_sketch(self, sketch):
if Gui is None or sketch is None:
return False
try:
Gui.Selection.clearSelection()
except Exception:
pass
try:
Gui.Selection.addSelection(sketch)
except Exception:
try:
doc = getattr(sketch, "Document", None)
Gui.Selection.addSelection(getattr(doc, "Name", ""), getattr(sketch, "Name", ""))
except Exception:
pass
try:
active_gui_doc = getattr(Gui, "ActiveDocument", None)
if active_gui_doc is not None:
active_gui_doc.setEdit(getattr(sketch, "Name", ""))
return True
except Exception:
pass
return False
def create_user_path_sketch_from_selection(self):
try:
self._sync_options_from_widgets()
result = self.controller.create_user_path_sketch_from_selection()
sketch = result.get("sketch")
edited = self._select_and_edit_sketch(sketch)
sketch_name = getattr(sketch, "Label", "") or getattr(sketch, "Name", "") or "布线路径草图"
edit_text = "已进入草图编辑;" if edited else "已选中新草图,可双击进入编辑;"
self._set_status(
"已创建{0}{1}请在草图里画布线路径线,退出草图后直接点击“生成布线路径网络”即可自动生成/刷新 UserPath如果要立即转换当前草图也可以选中它点击“选中路径作为用户路径”。{2}".format(
sketch_name,
edit_text,
self.controller.summary(),
)
)
except Exception as exc:
self._set_error(str(exc))
def create_user_path_from_selected_points(self):
try:
self._sync_options_from_widgets()
result = self.controller.create_user_path_from_selected_points()
count = result.get("user_path_carriers", 0)
point_count = result.get("user_path_point_count", 0)
if count <= 0:
self._set_status(
"未生成 3D 用户路径。请按顺序选择至少两个 3D 点、顶点或边端点。"
+ self.controller.summary()
)
return
self._set_status(
"已生成 3D 用户路径:{0} 条,路径点 {1} 个。重新生成布线路径网络后会参与自动布线。{2}".format(
count,
point_count,
self.controller.summary(),
)
)
except Exception as exc:
self._set_error(str(exc))
def create_orthogonal_user_path_from_selected_points(self):
try:
self._sync_options_from_widgets()
result = self.controller.create_orthogonal_user_path_from_selected_points()
count = result.get("user_path_carriers", 0)
point_count = result.get("user_path_point_count", 0)
if count <= 0:
self._set_status(
"未生成正交 3D 用户路径。请按顺序选择至少两个 3D 点、顶点或边端点。"
+ self.controller.summary()
)
return
self._set_status(
"已生成正交 3D 用户路径:{0} 条,路径点 {1} 个。路径只沿 X/Y/Z 单轴折线前进,重新生成布线路径网络后会参与自动布线。{2}".format(
count,
point_count,
self.controller.summary(),
)
)
except Exception as exc:
self._set_error(str(exc))
def create_user_paths_from_selection(self): def create_user_paths_from_selection(self):
try: try:
self._sync_options_from_widgets() self._sync_options_from_widgets()

@ -57,6 +57,7 @@ DEFAULT_TERMINAL_ACCESS_COMPONENT_SEGMENT_PENALTY = 25.0
DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY = 1000.0 DEFAULT_TERMINAL_ACCESS_FALLBACK_ONLY_COMPONENT_PENALTY = 1000.0
DEFAULT_TERMINAL_ACCESS_FALLBACK_CARRIER_PENALTY = 5000.0 DEFAULT_TERMINAL_ACCESS_FALLBACK_CARRIER_PENALTY = 5000.0
DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY = 2000.0 DEFAULT_TERMINAL_ACCESS_ENTRY_CANDIDATE_PENALTY = 2000.0
DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE = 10.0
DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0 DEFAULT_ADJOINING_DUCT_TOLERANCE = 5.0
DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0 DEFAULT_WIRING_CUT_OUT_BRIDGE_EXTENSION = 20.0
WIRE_DUCT_OBSTACLE_MODE = "PassThrough" WIRE_DUCT_OBSTACLE_MODE = "PassThrough"
@ -700,6 +701,70 @@ def _set_user_path_source_semantics(source):
) )
def _set_user_path_sketch_semantics(sketch, project_uuid="", support=None, sub_element_name="", offset=DEFAULT_ROUTE_PATH_FACE_OFFSET):
_set_user_path_source_semantics(sketch)
TerminalObjects.ensure_string_property(
sketch,
"QetProjectUuid",
PROPERTY_GROUP,
"Project UUID for this route sketch",
project_uuid,
)
TerminalObjects.ensure_string_property(
sketch,
"QetRouteSketchMode",
PROPERTY_GROUP,
"Manual route sketch mode",
"ManualUserPathSketch",
)
TerminalObjects.ensure_string_property(
sketch,
"QetRouteSketchSupportName",
PROPERTY_GROUP,
"One-shot support object used when creating this manual route sketch",
getattr(support, "Name", "") if support is not None else "",
)
TerminalObjects.ensure_string_property(
sketch,
"QetRouteSketchSupportLabel",
PROPERTY_GROUP,
"One-shot support label used when creating this manual route sketch",
getattr(support, "Label", "") if support is not None else "",
)
TerminalObjects.ensure_string_property(
sketch,
"QetRouteSketchSupportSubElement",
PROPERTY_GROUP,
"One-shot support sub-element used when creating this manual route sketch",
sub_element_name or "",
)
_ensure_float_property(
sketch,
"QetRouteSketchFaceOffsetMm",
"Offset from selected support face when creating this manual route sketch",
offset,
)
def _style_user_path_sketch(sketch):
view = getattr(sketch, "ViewObject", None)
if view is None:
return
for attr_name, value in (
("LineColor", (1.0, 0.85, 0.0)),
("ShapeColor", (1.0, 0.85, 0.0)),
("PointColor", (1.0, 0.85, 0.0)),
):
try:
setattr(view, attr_name, value)
except Exception:
pass
try:
view.LineWidth = 3.0
except Exception:
pass
def _object_has_bbox(obj): def _object_has_bbox(obj):
shape = getattr(obj, "Shape", None) shape = getattr(obj, "Shape", None)
return getattr(shape, "BoundBox", None) is not None return getattr(shape, "BoundBox", None) is not None
@ -1222,6 +1287,14 @@ def _object_global_placement(obj):
return getattr(obj, "Placement", None) return getattr(obj, "Placement", None)
def _route_source_geometry_placement(obj):
type_id = (getattr(obj, "TypeId", "") or "").lower()
if "sketch" in type_id:
# 真实 Sketcher::SketchObject 的 Shape 点已经带有 Attachment/Placement不能再平移一次。
return None
return _object_global_placement(obj)
def _points_with_placement(points, placement): def _points_with_placement(points, placement):
return [_placement_mult_vec(placement, _vector(point)) for point in points] return [_placement_mult_vec(placement, _vector(point)) for point in points]
@ -1283,6 +1356,13 @@ def _routing_source_text(obj):
).lower() ).lower()
def _is_explicit_user_path_source(obj):
return (
(getattr(obj, "QetRoutingSourceKind", "") or "").strip() == ROUTE_CARRIER_KIND_USER_PATH
or (getattr(obj, "QetRouteSketchMode", "") or "").strip() == "ManualUserPathSketch"
)
def _bbox_aspect_ratio(bbox): def _bbox_aspect_ratio(bbox):
extents = sorted( extents = sorted(
(_bbox_extent(bbox, axis) for axis in ("x", "y", "z")), (_bbox_extent(bbox, axis) for axis in ("x", "y", "z")),
@ -1300,6 +1380,8 @@ def _is_wire_duct_candidate(obj, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT):
return False return False
if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj):
return False return False
if _is_explicit_user_path_source(obj):
return False
if (getattr(obj, "RouteType", "") or "").strip(): if (getattr(obj, "RouteType", "") or "").strip():
return False return False
text = _routing_source_text(obj) text = _routing_source_text(obj)
@ -1345,6 +1427,8 @@ def _is_support_surface_candidate(obj):
return False return False
if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj):
return False return False
if _is_explicit_user_path_source(obj):
return False
if (getattr(obj, "RouteType", "") or "").strip(): if (getattr(obj, "RouteType", "") or "").strip():
return False return False
text = _routing_source_text(obj) text = _routing_source_text(obj)
@ -1372,6 +1456,8 @@ def _is_wiring_cut_out_candidate(obj):
return False return False
if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj): if is_route_carrier(obj) or is_routing_boundary(obj) or TerminalObjects.is_terminal_object(obj):
return False return False
if _is_explicit_user_path_source(obj):
return False
if (getattr(obj, "RouteType", "") or "").strip(): if (getattr(obj, "RouteType", "") or "").strip():
return False return False
@ -1437,6 +1523,7 @@ def _point_runs_from_selection_item(selection_item):
points = [] points = []
obj = getattr(selection_item, "Object", None) obj = getattr(selection_item, "Object", None)
placement = _object_global_placement(obj) placement = _object_global_placement(obj)
geometry_placement = _route_source_geometry_placement(obj)
for point in list(getattr(selection_item, "PickedPoints", []) or []): for point in list(getattr(selection_item, "PickedPoints", []) or []):
points.append(_vector(point)) points.append(_vector(point))
@ -1447,19 +1534,19 @@ def _point_runs_from_selection_item(selection_item):
if points: if points:
runs.append(points) runs.append(points)
points = [] points = []
runs.append(_points_with_placement(_wire_points(sub_object), placement)) runs.append(_points_with_placement(_wire_points(sub_object), geometry_placement))
continue continue
if shape_type == "edge": if shape_type == "edge":
points.extend(_points_with_placement(_edge_points(sub_object), placement)) points.extend(_points_with_placement(_edge_points(sub_object), geometry_placement))
continue continue
if shape_type == "vertex": if shape_type == "vertex":
point = getattr(sub_object, "Point", None) point = getattr(sub_object, "Point", None)
if point is not None: if point is not None:
points.append(_placement_mult_vec(placement, _vector(point))) points.append(_placement_mult_vec(geometry_placement, _vector(point)))
continue continue
center = _shape_center(sub_object) center = _shape_center(sub_object)
if center is not None: if center is not None:
points.append(_placement_mult_vec(placement, center)) points.append(_placement_mult_vec(geometry_placement, center))
if obj is not None and _is_route_path_source_object(obj): if obj is not None and _is_route_path_source_object(obj):
for point in list(getattr(obj, "Points", []) or []): for point in list(getattr(obj, "Points", []) or []):
@ -1473,14 +1560,14 @@ def _point_runs_from_selection_item(selection_item):
runs.append(points) runs.append(points)
points = [] points = []
for wire in wires: for wire in wires:
runs.append(_points_with_placement(_wire_points(wire), placement)) runs.append(_points_with_placement(_wire_points(wire), geometry_placement))
else: else:
for edge in list(getattr(shape, "Edges", []) or []): for edge in list(getattr(shape, "Edges", []) or []):
points.extend(_points_with_placement(_edge_points(edge), placement)) points.extend(_points_with_placement(_edge_points(edge), geometry_placement))
if not runs and not points: if not runs and not points:
center = _shape_center(shape) center = _shape_center(shape)
if center is not None: if center is not None:
points.append(_placement_mult_vec(placement, center)) points.append(_placement_mult_vec(geometry_placement, center))
if points: if points:
runs.append(points) runs.append(points)
@ -1508,6 +1595,21 @@ def _support_face_from_selection(selection_ex):
return None return None
def _support_face_selection_from_selection(selection_ex):
for item in selection_ex or []:
source = getattr(item, "Object", None)
sub_names = list(getattr(item, "SubElementNames", []) or [])
for index, sub_object in enumerate(list(getattr(item, "SubObjects", []) or [])):
if (getattr(sub_object, "ShapeType", "") or "").lower() != "face":
continue
return {
"face": sub_object,
"source": source,
"sub_element_name": sub_names[index] if index < len(sub_names) else "",
}
return None
def _selection_item_is_only_support_face(selection_item): def _selection_item_is_only_support_face(selection_item):
sub_objects = list(getattr(selection_item, "SubObjects", []) or []) sub_objects = list(getattr(selection_item, "SubObjects", []) or [])
if not sub_objects: if not sub_objects:
@ -1703,6 +1805,216 @@ def _project_points_to_face(points, face, offset=DEFAULT_ROUTE_PATH_FACE_OFFSET)
return projected return projected
def _rotation_from_face_normal(normal):
try:
rotation = App.Rotation(App.Vector(0, 0, 1), normal)
if rotation is not None:
return rotation
except Exception:
pass
try:
return App.Rotation()
except Exception:
return None
def _attach_sketch_to_support_face(sketch, support, sub_element_name, offset, fallback_base, normal):
if sketch is None:
return
attached = False
if support is not None and sub_element_name:
try:
sketch.AttachmentSupport = [(support, sub_element_name)]
sketch.MapMode = "FlatFace"
sketch.AttachmentOffset = App.Placement(
App.Vector(0, 0, abs(float(offset or 0.0))),
App.Rotation(),
)
attached = True
except Exception:
attached = False
try:
sketch.Placement = App.Placement(fallback_base, _rotation_from_face_normal(normal))
except Exception:
pass
if attached:
# Attachment 是真实 FreeCAD 里的编辑语义Placement 仍保留为测试和旧对象兜底。
return
def create_user_path_sketch_from_selection(
doc,
selection_ex,
project_uuid="",
offset=DEFAULT_ROUTE_PATH_FACE_OFFSET,
):
"""Create a one-shot Sketcher sketch on the selected cabinet/support face for manual UserPath drawing."""
if doc is None:
raise RoutingNetworkError("没有可用的 FreeCAD 文档。")
support = _support_face_selection_from_selection(selection_ex)
if support is None:
raise RoutingNetworkError("请先选中机柜面板、安装板、门板或线槽上的一个面 Face再创建布线路径草图。")
face = support["face"]
normal = _normalize(_face_normal(face))
if normal is None:
raise RoutingNetworkError("选中的 Face 无法确定法向,不能创建布线路径草图。")
face_points = _face_points(face)
origin = _face_origin(face, face_points)
base = _add(origin, _scale(normal, abs(float(offset or 0.0))))
name = _unique_name(doc, "QETUserPathSketch")
sketch = doc.addObject("Sketcher::SketchObject", name)
sketch.Label = "布线路径草图"
_attach_sketch_to_support_face(
sketch,
support.get("source"),
support.get("sub_element_name", ""),
abs(float(offset or 0.0)),
base,
normal,
)
_set_user_path_sketch_semantics(
sketch,
project_uuid=project_uuid,
support=support.get("source"),
sub_element_name=support.get("sub_element_name", ""),
offset=abs(float(offset or 0.0)),
)
_style_user_path_sketch(sketch)
try:
doc.recompute()
except Exception:
pass
return {
"sketch": sketch,
"support": support.get("source"),
"sub_element_name": support.get("sub_element_name", ""),
"offset": abs(float(offset or 0.0)),
}
def _selected_points_from_selection(selection_ex):
points = []
for item in selection_ex or []:
obj = getattr(item, "Object", None)
placement = _object_global_placement(obj)
geometry_placement = _route_source_geometry_placement(obj)
for point in list(getattr(item, "PickedPoints", []) or []):
points.append(_vector(point))
for sub_object in list(getattr(item, "SubObjects", []) or []):
shape_type = (getattr(sub_object, "ShapeType", "") or "").lower()
if shape_type == "wire":
points.extend(_points_with_placement(_wire_points(sub_object), geometry_placement))
continue
if shape_type == "edge":
points.extend(_points_with_placement(_edge_points(sub_object), geometry_placement))
continue
if shape_type == "vertex":
point = getattr(sub_object, "Point", None)
if point is not None:
points.append(_placement_mult_vec(geometry_placement, _vector(point)))
continue
center = _shape_center(sub_object)
if center is not None:
points.append(_placement_mult_vec(geometry_placement, center))
if obj is not None and _is_route_path_source_object(obj):
for point in list(getattr(obj, "Points", []) or []):
points.append(_placement_mult_vec(placement, _vector(point)))
return _normalize_point_run(points)
def create_user_path_carrier_from_selected_points(doc, selection_ex, project_uuid="", label="QET 3D User Route Path"):
"""Create one 3D UserPath from selected vertices, picked points, or point-like sub-objects in selection order."""
if doc is None:
raise RoutingNetworkError("没有可用的 FreeCAD 文档。")
points = _selected_points_from_selection(selection_ex)
if len(points) < 2:
raise RoutingNetworkError("请至少按顺序选择两个 3D 点、顶点、边端点或带 Points 的路径对象。")
carrier = create_route_carrier(
doc,
points,
label=label,
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_USER_PATH,
)
try:
doc.recompute()
except Exception:
pass
return {
"carrier": carrier,
"points": points,
}
def _orthogonal_points_between(start, end, axis_order=("x", "y", "z")):
current = _vector(start)
end = _vector(end)
points = [current]
for axis in axis_order or ("x", "y", "z"):
if abs(_axis_value(current, axis) - _axis_value(end, axis)) <= DEFAULT_NODE_TOLERANCE:
continue
current = _set_axis(current, axis, _axis_value(end, axis))
if _distance(points[-1], current) > DEFAULT_NODE_TOLERANCE:
points.append(current)
if _distance(points[-1], end) > DEFAULT_NODE_TOLERANCE:
points.append(end)
return points
def _orthogonalize_points(points, axis_order=("x", "y", "z")):
source_points = _normalize_point_run([_vector(point) for point in points or []])
if len(source_points) < 2:
return source_points
result = [source_points[0]]
for end in source_points[1:]:
segment_points = _orthogonal_points_between(result[-1], end, axis_order=axis_order)
for point in segment_points[1:]:
if _distance(result[-1], point) > DEFAULT_NODE_TOLERANCE:
result.append(point)
return _normalize_point_run(result)
def create_orthogonal_user_path_carrier_from_selected_points(
doc,
selection_ex,
project_uuid="",
label="QET Orthogonal 3D User Route Path",
axis_order=("x", "y", "z"),
):
"""Create one X/Y/Z orthogonal 3D UserPath from selected points in selection order."""
if doc is None:
raise RoutingNetworkError("没有可用的 FreeCAD 文档。")
points = _selected_points_from_selection(selection_ex)
if len(points) < 2:
raise RoutingNetworkError("请至少按顺序选择两个 3D 点、顶点、边端点或带 Points 的路径对象。")
orthogonal_points = _orthogonalize_points(points, axis_order=axis_order)
carrier = create_route_carrier(
doc,
orthogonal_points,
label=label,
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_USER_PATH,
)
TerminalObjects.ensure_string_property(
carrier,
"QetRoutePathMode",
PROPERTY_GROUP,
"Manual 3D route path mode",
"Orthogonal3D",
)
try:
doc.recompute()
except Exception:
pass
return {
"carrier": carrier,
"points": orthogonal_points,
}
def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUTE_CARRIER_KIND): def create_carriers_from_selection(doc, selection_ex, project_uuid="", kind=ROUTE_CARRIER_KIND):
created = [] created = []
support_face = _support_face_from_selection(selection_ex) support_face = _support_face_from_selection(selection_ex)
@ -1762,6 +2074,11 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="")
point_runs = [_normalize_point_run(points) for points in point_runs] point_runs = [_normalize_point_run(points) for points in point_runs]
point_runs = [points for points in point_runs if len(points) >= 2] point_runs = [points for points in point_runs if len(points) >= 2]
if not point_runs: if not point_runs:
if source is not None:
live_carriers = _live_source_carriers(doc, source)
if live_carriers:
_remove_route_carriers(doc, live_carriers)
_mark_user_path_source_carriers(source, [])
continue continue
label = "QET User Route Path {0}".format(index) label = "QET User Route Path {0}".format(index)
@ -1821,6 +2138,70 @@ def create_user_path_carriers_from_selection(doc, selection_ex, project_uuid="")
return created return created
def _create_or_refresh_user_path_source(doc, source, project_uuid="", label_prefix="QET User Route Path"):
if source is None or is_route_carrier(source) or is_routing_boundary(source):
return []
point_runs = _point_runs_from_selection_item(type("_Selection", (), {"Object": source, "SubObjects": []})())
point_runs = [_normalize_point_run(points) for points in point_runs]
point_runs = [points for points in point_runs if len(points) >= 2]
if not point_runs:
live_carriers = _live_source_carriers(doc, source)
if live_carriers:
_remove_route_carriers(doc, live_carriers)
_mark_user_path_source_carriers(source, [])
return []
label = "{0} {1}".format(
label_prefix,
getattr(source, "Label", "") or getattr(source, "Name", "") or "",
).strip()
capacity = _route_carrier_capacity_value(source, default=1)
live_carriers = _live_source_carriers(doc, source)
refreshed = []
for run_index, points in enumerate(point_runs, start=1):
run_label = label if len(point_runs) == 1 else "{0} {1}".format(label, run_index)
if run_index <= len(live_carriers):
carrier = live_carriers[run_index - 1]
if _update_route_carrier(
carrier,
points,
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_USER_PATH,
capacity=capacity,
):
refreshed.append(carrier)
continue
refreshed.append(
create_route_carrier(
doc,
points,
label=run_label,
project_uuid=project_uuid,
kind=ROUTE_CARRIER_KIND_USER_PATH,
capacity=capacity,
)
)
if len(live_carriers) > len(point_runs):
_remove_route_carriers(doc, live_carriers[len(point_runs) :])
_mark_user_path_source_carriers(source, refreshed)
return refreshed
def create_user_path_carriers_from_document(doc, project_uuid=""):
"""Create or refresh UserPath carriers from all sketch/Draft-like path sources in the document."""
cleanup_invalid_source_carriers(doc)
created = []
for source in detect_document_user_path_sources(doc):
if (
_is_wire_duct_candidate(source)
or _is_support_surface_candidate(source)
or _is_wiring_cut_out_candidate(source)
):
continue
created.extend(_create_or_refresh_user_path_source(doc, source, project_uuid=project_uuid))
return created
def _nearest_points_between_route_point_runs(left_points, right_points): def _nearest_points_between_route_point_runs(left_points, right_points):
left_points = _normalized_route_points(left_points) left_points = _normalized_route_points(left_points)
right_points = _normalized_route_points(right_points) right_points = _normalized_route_points(right_points)
@ -2357,6 +2738,8 @@ def _set_route_carrier_source_metadata(carrier, source, source_kind="", source_p
def _remember_source_carriers(source, carriers): def _remember_source_carriers(source, carriers):
if source is None:
return
live_names = [ live_names = [
getattr(carrier, "Name", "") getattr(carrier, "Name", "")
for carrier in (carriers or []) for carrier in (carriers or [])
@ -2377,13 +2760,13 @@ def _remember_source_carriers(source, carriers):
source_kind=source_kind, source_kind=source_kind,
source_path_index=source_path_index, source_path_index=source_path_index,
) )
TerminalObjects.ensure_string_property( TerminalObjects.ensure_string_property(
source, source,
"QetRouteCarrierNamesJson", "QetRouteCarrierNamesJson",
PROPERTY_GROUP, PROPERTY_GROUP,
"Generated route carriers for this source", "Generated route carriers for this source",
json.dumps(live_names, ensure_ascii=False), json.dumps(live_names, ensure_ascii=False),
) )
def _mark_wire_duct_source(source, carrier, carriers=None, end_margin=DEFAULT_WIRE_DUCT_MARGIN): def _mark_wire_duct_source(source, carrier, carriers=None, end_margin=DEFAULT_WIRE_DUCT_MARGIN):
@ -2457,7 +2840,7 @@ def _mark_user_path_source(source, carrier):
def _mark_user_path_source_carriers(source, carriers): def _mark_user_path_source_carriers(source, carriers):
carriers = [carrier for carrier in (carriers or []) if carrier is not None] carriers = [carrier for carrier in (carriers or []) if carrier is not None]
if source is None or not carriers: if source is None:
return return
try: try:
_set_user_path_source_semantics(source) _set_user_path_source_semantics(source)
@ -2466,7 +2849,7 @@ def _mark_user_path_source_carriers(source, carriers):
"QetRouteCarrierName", "QetRouteCarrierName",
PROPERTY_GROUP, PROPERTY_GROUP,
"Generated route carrier for this source", "Generated route carrier for this source",
getattr(carriers[0], "Name", ""), getattr(carriers[0], "Name", "") if carriers else "",
) )
_remember_source_carriers(source, carriers) _remember_source_carriers(source, carriers)
except Exception: except Exception:
@ -2626,6 +3009,47 @@ def detect_user_path_sources(doc):
return sources return sources
def _is_document_user_path_source(obj):
if not _is_route_path_source_object(obj):
return False
if _is_explicit_user_path_source(obj):
return True
text = " ".join(
str(value or "")
for value in (
getattr(obj, "Name", ""),
getattr(obj, "Label", ""),
)
).lower()
return any(
keyword in text
for keyword in (
"userpath",
"user path",
"route path",
"routing path",
"wire path",
"wiring path",
"布线路径",
"走线路径",
"用户路径",
)
)
def detect_document_user_path_sources(doc):
"""Return path sources that are safe to auto-convert during full network generation."""
sources = []
seen = set()
for obj in list(getattr(doc, "Objects", []) or []):
if id(obj) in seen:
continue
seen.add(id(obj))
if _is_document_user_path_source(obj):
sources.append(obj)
return sources
def _source_sample(obj): def _source_sample(obj):
return { return {
"name": getattr(obj, "Name", ""), "name": getattr(obj, "Name", ""),
@ -3060,6 +3484,68 @@ def _terminal_parent_chain(terminal):
return chain return chain
def _bbox_volume(bbox):
try:
return (
max(float(bbox.XMax) - float(bbox.XMin), 0.0)
* max(float(bbox.YMax) - float(bbox.YMin), 0.0)
* max(float(bbox.ZMax) - float(bbox.ZMin), 0.0)
)
except Exception:
return float("inf")
def _terminal_parent_device_bbox(terminal, origin):
candidates = []
seen = set()
pending = list(getattr(terminal, "InList", []) or [])
while pending:
parent = pending.pop(0)
if parent is None or id(parent) in seen:
continue
seen.add(id(parent))
bbox = _bound_box_from_object(parent)
if bbox is not None and _point_inside_bbox(origin, bbox, tolerance=DEFAULT_NODE_TOLERANCE):
candidates.append(bbox)
pending.extend(list(getattr(parent, "InList", []) or []))
if not candidates:
return None
return min(candidates, key=_bbox_volume)
def _ray_exit_distance_from_bbox(origin, direction, bbox):
if not _point_inside_bbox(origin, bbox, tolerance=DEFAULT_NODE_TOLERANCE):
return None
distances = []
for axis in ("x", "y", "z"):
component = _axis_value(direction, axis)
if abs(component) <= DEFAULT_NODE_TOLERANCE:
continue
low, high = _bbox_axis_range(bbox, axis)
boundary = high if component > 0 else low
distance = (boundary - _axis_value(origin, axis)) / component
if distance >= -DEFAULT_NODE_TOLERANCE:
distances.append(max(float(distance), 0.0))
if not distances:
return None
return min(distances)
def _terminal_device_aware_exit_point(terminal, exit_length):
origin = _vector(TerminalObjects.terminal_origin(terminal))
direction = _normalize(_vector(TerminalObjects.terminal_direction(terminal)))
if direction is None:
direction = App.Vector(0, 0, 1)
length = max(float(exit_length or 0.0), 0.0)
bbox = _terminal_parent_device_bbox(terminal, origin)
exit_distance = _ray_exit_distance_from_bbox(origin, direction, bbox)
if exit_distance is not None:
# 没有人工局部路径时,默认出线至少先离开所属设备外轮廓,避免导线贴在模型内部。
length = max(length, exit_distance + DEFAULT_TERMINAL_DEVICE_EXIT_CLEARANCE)
return _add(origin, _scale(direction, length))
def _terminal_local_point_to_global(terminal, local_point): def _terminal_local_point_to_global(terminal, local_point):
try: try:
if hasattr(terminal, "getGlobalPlacement"): if hasattr(terminal, "getGlobalPlacement"):
@ -3176,7 +3662,7 @@ def terminal_access_path_points(terminal, exit_length=20.0):
normalized = _normalized_route_points(points) normalized = _normalized_route_points(points)
if len(normalized) >= 2: if len(normalized) >= 2:
return normalized return normalized
return _normalized_route_points([origin, _terminal_exit_point(terminal, exit_length)]) return _normalized_route_points([origin, _terminal_device_aware_exit_point(terminal, exit_length)])
def _orthogonal_access_points(start, end): def _orthogonal_access_points(start, end):
@ -3491,6 +3977,10 @@ def create_routing_path_network_from_document(
selection_ex, selection_ex,
project_uuid=project_uuid, project_uuid=project_uuid,
) )
document_user_paths = create_user_path_carriers_from_document(
doc,
project_uuid=project_uuid,
)
wire_ducts = create_wire_duct_carriers_from_document( wire_ducts = create_wire_duct_carriers_from_document(
doc, doc,
project_uuid=project_uuid, project_uuid=project_uuid,
@ -3520,6 +4010,13 @@ def create_routing_path_network_from_document(
for carrier in selected_wire_ducts for carrier in selected_wire_ducts
if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT
) )
all_user_paths = []
seen_user_paths = set()
for carrier in list(selected_user_paths) + list(document_user_paths):
if carrier is None or id(carrier) in seen_user_paths:
continue
seen_user_paths.add(id(carrier))
all_user_paths.append(carrier)
open_end_count = sum( open_end_count = sum(
1 1
for carrier in all_wire_duct_created for carrier in all_wire_duct_created
@ -3529,7 +4026,9 @@ def create_routing_path_network_from_document(
return { return {
"wire_duct_carriers": wire_duct_main_count, "wire_duct_carriers": wire_duct_main_count,
"selected_wire_duct_carriers": selected_wire_duct_main_count, "selected_wire_duct_carriers": selected_wire_duct_main_count,
"user_path_carriers": len(selected_user_paths), "user_path_carriers": len(all_user_paths),
"selected_user_path_carriers": len(selected_user_paths),
"document_user_path_carriers": len(document_user_paths),
"wire_duct_open_end_carriers": open_end_count, "wire_duct_open_end_carriers": open_end_count,
"wiring_cut_out_carriers": len(cut_outs), "wiring_cut_out_carriers": len(cut_outs),
"surface_carriers": len(surfaces), "surface_carriers": len(surfaces),

@ -150,6 +150,8 @@ class FakeShape:
class FakeVertex: class FakeVertex:
ShapeType = "Vertex"
def __init__(self, point): def __init__(self, point):
self.Point = point self.Point = point
@ -197,8 +199,9 @@ class FakeFace:
class FakeSelectionItem: class FakeSelectionItem:
def __init__(self, sub_objects=None, obj=None): def __init__(self, sub_objects=None, obj=None, sub_element_names=None):
self.SubObjects = sub_objects or [] self.SubObjects = sub_objects or []
self.SubElementNames = sub_element_names or []
self.PickedPoints = [] self.PickedPoints = []
self.Object = obj self.Object = obj
@ -2824,7 +2827,7 @@ class AutoRoutingTest(unittest.TestCase):
[(point.x, point.y, point.z) for point in carriers[0].Points], [(point.x, point.y, point.z) for point in carriers[0].Points],
) )
def test_selected_user_path_shape_points_honor_object_placement(self): def test_selected_user_path_shape_points_honor_object_placement_for_non_sketch_path(self):
_install_fake_freecad() _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") auto_routing_panel = importlib.import_module("AutoRoutingPanel")
@ -2833,7 +2836,7 @@ class AutoRoutingTest(unittest.TestCase):
doc = FakeDocument() doc = FakeDocument()
app.ActiveDocument = doc app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1") terminal_objects.ensure_root_group(doc, "project-1")
route_path = doc.addObject("Sketcher::SketchObject", "MovedRouteSketch") route_path = doc.addObject("Part::Feature", "MovedRoutePath")
route_path.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) route_path.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation())
route_path.Shape = FakeShape( route_path.Shape = FakeShape(
FakeBoundBox(0, 50, 0, 50, 20, 20), FakeBoundBox(0, 50, 0, 50, 20, 20),
@ -2861,6 +2864,296 @@ class AutoRoutingTest(unittest.TestCase):
[(point.x, point.y, point.z) for point in carriers[0].Points], [(point.x, point.y, point.z) for point in carriers[0].Points],
) )
def test_selected_sketch_user_path_shape_points_are_not_placement_shifted_twice(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
route_path = doc.addObject("Sketcher::SketchObject", "PlacedRouteSketch")
route_path.Placement = app.Placement(app.Vector(100, 10, 35), app.Rotation())
route_path.Shape = FakeShape(
FakeBoundBox(100, 150, 10, 60, 35, 35),
wires=[
FakeWire(
[
app.Vector(100, 10, 35),
app.Vector(150, 10, 35),
app.Vector(150, 60, 35),
]
)
],
)
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)],
)
result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection()
carriers = routing_network.collect_route_carriers(doc)
self.assertEqual(1, result["user_path_carriers"])
self.assertEqual(
[(100.0, 10.0, 35.0), (150.0, 10.0, 35.0), (150.0, 60.0, 35.0)],
[(point.x, point.y, point.z) for point in carriers[0].Points],
)
def test_controller_creates_face_based_user_path_sketch_for_manual_drawing(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
face = FakeFace(
FakeBoundBox(0, 100, 0, 60, 10, 10),
app.Vector(0, 0, 1),
vertices=[
app.Vector(0, 0, 10),
app.Vector(100, 0, 10),
app.Vector(100, 60, 10),
app.Vector(0, 60, 10),
],
)
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [FakeSelectionItem(obj=panel, sub_objects=[face], sub_element_names=["Face1"])],
)
result = auto_routing_panel.AutoRoutingController().create_user_path_sketch_from_selection()
sketch = result["sketch"]
self.assertEqual(1, result["user_path_sketches"])
self.assertEqual("Sketcher::SketchObject", sketch.TypeId)
self.assertEqual("UserPath", sketch.QetRoutingSourceKind)
self.assertEqual("project-1", sketch.QetProjectUuid)
self.assertEqual("MountingPlateA", sketch.QetRouteSketchSupportName)
self.assertEqual("Face1", sketch.QetRouteSketchSupportSubElement)
self.assertEqual([(panel, "Face1")], sketch.AttachmentSupport)
self.assertEqual("FlatFace", sketch.MapMode)
self.assertEqual(12.0, sketch.Placement.Base.z)
def test_controller_creates_face_based_user_path_sketch_with_configured_offset(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
panel = doc.addObject("Part::Feature", "MountingPlateA")
face = FakeFace(
FakeBoundBox(0, 100, 0, 60, 10, 10),
app.Vector(0, 0, 1),
vertices=[
app.Vector(0, 0, 10),
app.Vector(100, 0, 10),
app.Vector(100, 60, 10),
app.Vector(0, 60, 10),
],
)
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [FakeSelectionItem(obj=panel, sub_objects=[face], sub_element_names=["Face1"])],
)
controller = auto_routing_panel.AutoRoutingController()
controller.set_user_path_sketch_offset(50.0)
result = controller.create_user_path_sketch_from_selection()
sketch = result["sketch"]
self.assertEqual(60.0, sketch.Placement.Base.z)
self.assertEqual(50.0, sketch.QetRouteSketchFaceOffsetMm)
self.assertEqual(50.0, sketch.AttachmentOffset.Base.z)
def test_controller_creates_3d_user_path_from_selected_vertices_in_selection_order(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
source_a = doc.addObject("Part::Feature", "PointA")
source_b = doc.addObject("Part::Feature", "PointB")
source_c = doc.addObject("Part::Feature", "PointC")
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [
FakeSelectionItem(obj=source_a, sub_objects=[FakeVertex(app.Vector(0, 0, 10))]),
FakeSelectionItem(obj=source_b, sub_objects=[FakeVertex(app.Vector(0, 50, 10))]),
FakeSelectionItem(obj=source_c, sub_objects=[FakeVertex(app.Vector(80, 50, 40))]),
],
)
result = auto_routing_panel.AutoRoutingController().create_user_path_from_selected_points()
carriers = routing_network.collect_route_carriers(doc)
self.assertEqual(1, result["user_path_carriers"])
self.assertEqual(
[(0.0, 0.0, 10.0), (0.0, 50.0, 10.0), (80.0, 50.0, 40.0)],
[(point.x, point.y, point.z) for point in carriers[0].Points],
)
def test_controller_creates_orthogonal_3d_user_path_from_selected_vertices(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
source_a = doc.addObject("Part::Feature", "PointA")
source_b = doc.addObject("Part::Feature", "PointB")
source_c = doc.addObject("Part::Feature", "PointC")
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [
FakeSelectionItem(obj=source_a, sub_objects=[FakeVertex(app.Vector(0, 0, 10))]),
FakeSelectionItem(obj=source_b, sub_objects=[FakeVertex(app.Vector(0, 50, 10))]),
FakeSelectionItem(obj=source_c, sub_objects=[FakeVertex(app.Vector(80, 50, 40))]),
],
)
result = auto_routing_panel.AutoRoutingController().create_orthogonal_user_path_from_selected_points()
carriers = routing_network.collect_route_carriers(doc)
self.assertEqual(1, result["user_path_carriers"])
self.assertEqual(4, result["user_path_point_count"])
self.assertEqual("Orthogonal3D", carriers[0].QetRoutePathMode)
self.assertEqual(
[
(0.0, 0.0, 10.0),
(0.0, 50.0, 10.0),
(80.0, 50.0, 10.0),
(80.0, 50.0, 40.0),
],
[(point.x, point.y, point.z) for point in carriers[0].Points],
)
def test_generate_routing_paths_discovers_drawn_user_path_sketch_without_selecting_it(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
route_path = doc.addObject("Sketcher::SketchObject", "DrawnUserPathSketch")
route_path.Shape = FakeShape(
FakeBoundBox(0, 120, 0, 80, 20, 20),
wires=[
FakeWire(
[
app.Vector(0, 0, 20),
app.Vector(0, 60, 20),
app.Vector(120, 60, 20),
]
)
],
)
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [],
)
result = auto_routing_panel.AutoRoutingController().generate_routing_paths()
carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if carrier.QetRouteCarrierKind == "UserPath"
]
self.assertEqual(1, result["user_path_carriers"])
self.assertEqual(1, len(carriers))
self.assertEqual(
[(0.0, 0.0, 20.0), (0.0, 60.0, 20.0), (120.0, 60.0, 20.0)],
[(point.x, point.y, point.z) for point in carriers[0].Points],
)
def test_generate_routing_paths_does_not_auto_convert_unmarked_mechanical_sketch(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
mechanical_sketch = doc.addObject("Sketcher::SketchObject", "Sketch001")
mechanical_sketch.Shape = FakeShape(
FakeBoundBox(0, 120, 0, 80, 20, 20),
wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(120, 0, 20)])],
)
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [],
)
result = auto_routing_panel.AutoRoutingController().generate_routing_paths()
carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if carrier.QetRouteCarrierKind == "UserPath"
]
self.assertEqual(0, result["user_path_carriers"])
self.assertEqual([], carriers)
def test_marked_user_path_sketch_is_not_skipped_when_name_mentions_support_surface(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
auto_routing_panel = importlib.import_module("AutoRoutingPanel")
app = sys.modules["FreeCAD"]
gui = sys.modules["FreeCADGui"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
route_path = doc.addObject("Sketcher::SketchObject", "MountingPlateRouteSketch")
route_path.Label = "安装板布线路径草图"
route_path.QetRoutingSourceKind = "UserPath"
route_path.QetRouteSketchMode = "ManualUserPathSketch"
route_path.Shape = FakeShape(
FakeBoundBox(0, 120, 0, 80, 20, 20),
wires=[
FakeWire(
[
app.Vector(0, 0, 20),
app.Vector(0, 80, 20),
app.Vector(120, 80, 20),
]
)
],
)
gui.Selection = types.SimpleNamespace(
getSelection=lambda: [],
getSelectionEx=lambda: [],
)
result = auto_routing_panel.AutoRoutingController().generate_routing_paths()
carriers = [
carrier
for carrier in routing_network.collect_route_carriers(doc)
if carrier.QetRouteCarrierKind == "UserPath"
]
self.assertEqual(1, result["user_path_carriers"])
self.assertEqual(1, len(carriers))
self.assertEqual(
[(0.0, 0.0, 20.0), (0.0, 80.0, 20.0), (120.0, 80.0, 20.0)],
[(point.x, point.y, point.z) for point in carriers[0].Points],
)
def test_disconnected_shape_wires_create_separate_user_paths(self): def test_disconnected_shape_wires_create_separate_user_paths(self):
_install_fake_freecad() _install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
@ -2937,6 +3230,36 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("", carriers[0].QetRouteSourcePathIndex) self.assertEqual("", carriers[0].QetRouteSourcePathIndex)
self.assertEqual(1, len(json.loads(route_path.QetRouteCarrierNamesJson))) self.assertEqual(1, len(json.loads(route_path.QetRouteCarrierNamesJson)))
def test_refreshing_marked_user_path_with_no_drawn_lines_removes_stale_carriers(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
route_path = doc.addObject("Sketcher::SketchObject", "EditableUserPathSketch")
route_path.QetRoutingSourceKind = "UserPath"
route_path.QetRouteSketchMode = "ManualUserPathSketch"
route_path.Shape = FakeShape(
FakeBoundBox(0, 80, 0, 20, 20, 20),
wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(80, 20, 20)])],
)
first = routing_network.create_user_path_carriers_from_document(
doc,
project_uuid="project-1",
)
route_path.Shape = FakeShape(FakeBoundBox(0, 80, 0, 20, 20, 20), wires=[])
second = routing_network.create_user_path_carriers_from_document(
doc,
project_uuid="project-1",
)
carriers = routing_network.collect_route_carriers(doc)
self.assertEqual(1, len(first))
self.assertEqual([], second)
self.assertEqual([], carriers)
self.assertEqual([], json.loads(route_path.QetRouteCarrierNamesJson))
def test_refreshing_single_wire_user_path_adds_new_carriers_for_added_wires(self): def test_refreshing_single_wire_user_path_adds_new_carriers_for_added_wires(self):
_install_fake_freecad() _install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
@ -5115,6 +5438,22 @@ class AutoRoutingTest(unittest.TestCase):
[(point.x, point.y, point.z) for point in access_points], [(point.x, point.y, point.z) for point in access_points],
) )
def test_terminal_access_extends_past_parent_device_bbox_when_no_local_route_exists(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
device = doc.addObject("App::DocumentObjectGroup", "ProtectionDevice")
device.Shape = FakeShape(FakeBoundBox(-20, 20, -20, 20, -5, 60))
terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
device.addObject(terminal)
access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0)
self.assertEqual((0.0, 0.0, 0.0), (access_points[0].x, access_points[0].y, access_points[0].z))
self.assertGreater(access_points[-1].z, 60.0)
def test_generate_routing_paths_refreshes_selected_user_path_without_duplicate(self): def test_generate_routing_paths_refreshes_selected_user_path_without_duplicate(self):
_install_fake_freecad() _install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()

Loading…
Cancel
Save