feat(freecad): 支持主路径端点桥接布线

dev
Zhaowenlong 3 weeks ago
parent daf16df9d6
commit ec0f105c92

@ -254,9 +254,9 @@ QET 侧如果能提供端子排/断路器的顺序、数量和显示编号,则
构图时不要求所有 carrier 都提前手工打断。系统会识别轴向线段之间的几何相交和同线重叠,把交点/重叠端点自动切成图节点。这样多条线槽中心路径只要在空间中相交就可以在交点处换向Dijkstra 才能得到符合工程布线习惯的折线路径。
相邻线槽端点允许存在小间隙。默认情况下,两个 `WireDuct` 端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度和线槽端部留缝
相邻主路径端点允许存在小间隙。默认情况下,`WireDuct` / `UserPath` / `WiringCutOut` 等主路径端点距离不超过 5 mm 时会被视为相邻并自动桥接;自动布线选项 `adjoining_duct_tolerance` 可以按需要调大或调小,用于适配不同建模精度、线槽端部留缝,以及用户路径贴近线槽但未精确相交的情况
FreeCAD 的 `3D 布线连接` 面板提供“线槽桥接容差 mm”数值框手动测试时可直接调整这个选项生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。面板摘要和检查报告都会按当前容差显示自动桥接段数便于确认当前容差是否生效。
FreeCAD 的 `3D 布线连接` 面板提供“主路径桥接容差 mm”数值框手动测试时可直接调整这个选项生成布线路径网络、检查布线路径网络和生成布线连接都会读取当前面板值。内部字段名仍沿用 `adjoining_duct_tolerance` 以兼容已有代码,但界面语义已明确为主路径端点桥接。面板摘要和检查报告都会按当前容差显示自动桥接段数,便于确认当前容差是否生效。
同一面板还提供“端子接入最大距离 mm”和“端子出线长度 mm”。前者用于控制端子距离最近路由网络超过多少毫米时不再生成 `TerminalAccess`,避免设备还没摆放好时生成超长悬空接入线;后者用于控制端子沿 LCS 出线方向先走出的短线长度,避免导线从设备壳体内部或端子原点直接折返。
@ -543,7 +543,7 @@ QetWiringCutOutBridgeExtensionMm = 20.0
批量生成布线连接后,面板/控制台报告会从第一条可追踪路径中提取一条“路径示例”,显示导线经过的源对象标签,便于快速确认线路是否进入了预期线槽、过线孔和支撑面。
批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻线槽自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。
批量布线报告还会汇总本批次路线中使用到的路径网络特征:如果路线依赖相邻主路径自动桥接,报告会显示自动桥接段数;如果主动避障时屏蔽了穿过障碍包围盒的网络边,报告会显示避障屏蔽段数。这里采用路线中的最大值展示,避免多条导线共用同一网络时重复累加。
一键执行“生成布线连接”时,系统会在更新路径网络后附带一份 `routing_path_network_diagnostic` 摘要到批量报告中。即使用户没有单独点击路径网络检查,报告也会显示“路径网络检查提示”,把空路径网络、路径对象几何无效、仅使用布线面兜底、端子局部路径无效、端子接入过长等问题带出来。
@ -627,7 +627,7 @@ tests/python/freecad_exchange_auto_routing_test.py
18. “准备布线布局空间”始终按整份文档识别线槽、支撑面和工程端子,并标记障碍处理方式。
19. “生成布线路径网络”按 EPLAN 的 Generate routing path network 语义生成 `WireDuct` / `UserPath` / `WiringCutOut` / `RoutingRange` / `TerminalAccess` carrier有选择时选中线槽或草图路径只作为额外识别提示仍会扫描整份文档。
20. “生成布线连接”会先更新同一套布线路径网络,再按全部 QET 导线任务批量求路。
21. 相邻线槽端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。
21. 相邻主路径端点在容差内会被网络自动连通;端子接入会连接到最近的网络线段点,而不是只连接到已有端点。
22. 线槽端部会生成 `WireDuctOpenEnd` 横向路径,穿线孔/过线孔会生成 `WiringCutOut` carrier。
23. 导线会保存 routing track网络检查会生成 `RoutingPathNetwork` 诊断对象。
24. 自动生成的线槽、过线孔和支撑面 carrier 会在源对象移动、缩放、删除或失效后刷新/清理。
@ -635,8 +635,8 @@ tests/python/freecad_exchange_auto_routing_test.py
26. `QetRouteTrackJson` 会在 carrier 有源对象元数据时保存 `source_name`、`source_label`、`source_kind`,方便核对导线实际走过的线槽、过线孔或支撑面。
27. 批量布线报告会显示一条路径示例,列出首条可追踪导线经过的源对象标签。
28. 线槽源对象支持通过 `QetWireDuctEndMarginMm` 按对象调整中心路径端部缩进距离。
29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻线槽端点自动桥接容差,并在网络结果中记录桥接段数量。
30. `3D 布线连接` 面板提供“线槽桥接容差 mm”设置面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。
29. 自动布线支持通过 `adjoining_duct_tolerance` 调整相邻主路径端点自动桥接容差,并在网络结果中记录桥接段数量。
30. `3D 布线连接` 面板提供“主路径桥接容差 mm”设置面板生成/检查/布线流程会使用该值;网络检查报告会显示自动桥接段数。
31. `3D 布线连接` 面板提供“端子接入最大距离 mm”和“端子出线长度 mm”设置用于适配真实机柜里端子离线槽远近不同、设备端子方向不同的情况。
32. 布线路径网络检查会在端子未接入诊断中记录当前端子接入最大距离和端子出线长度,并在中文报告里显示最大接入距离。
33. 批量布线报告会显示路径网络自动桥接段数和主动避障屏蔽段数,方便核对调参和避障是否实际参与求路。
@ -663,7 +663,7 @@ tests/manual/freecad_auto_routing_smoke.py
3. 清除布线连接
4. 清除走线路径
5. 点击“准备布线布局空间”
6. 按当前机柜情况调整线槽桥接容差、端子接入最大距离、端子出线长度
6. 按当前机柜情况调整主路径桥接容差、端子接入最大距离、端子出线长度
7. 可选:选中无法自动识别的线槽实体
8. 点击“生成布线路径网络”;如果不选择,则使用整份文档自动识别
9. 点击“生成布线连接”

@ -2032,7 +2032,7 @@ def format_eplan_connection_route_report(report):
blocked_segments = _route_network_metric_max(report, "blocked_segments")
network_parts = []
if bridged_segments > 0:
network_parts.append("自动桥接 {0} 段相邻线槽".format(bridged_segments))
network_parts.append("自动桥接 {0} 段相邻主路径".format(bridged_segments))
if blocked_segments > 0:
network_parts.append("避障屏蔽 {0}".format(blocked_segments))
if network_parts:
@ -2475,7 +2475,7 @@ def format_routing_path_network_report(diagnostic):
)
bridged_segments = int(summary.get("bridged_segments", 0) or 0)
if bridged_segments > 0:
message += " 自动桥接 {0} 段相邻线槽".format(bridged_segments)
message += " 自动桥接 {0} 段相邻主路径".format(bridged_segments)
return message
message = "布线路径网络检查发现 {0} 类问题。".format(len(issues))

@ -289,7 +289,7 @@ class AutoRoutingTaskPanel:
layout = QtWidgets.QVBoxLayout(self.form)
options_layout = QtWidgets.QHBoxLayout()
options_layout.addWidget(QtWidgets.QLabel("线槽桥接容差 mm"))
options_layout.addWidget(QtWidgets.QLabel("主路径桥接容差 mm"))
self.adjoining_duct_tolerance_spin = QtWidgets.QDoubleSpinBox()
self.adjoining_duct_tolerance_spin.setRange(0.0, 1000.0)
self.adjoining_duct_tolerance_spin.setDecimals(1)

@ -23,6 +23,14 @@ ROUTE_CARRIER_KIND_USER_PATH = "UserPath"
ROUTE_CARRIER_KIND_AUXILIARY_PATH = "AuxiliaryPath"
ROUTE_CARRIER_KIND_ROUTING_RANGE = "RoutingRange"
ROUTE_CARRIER_KIND_TERMINAL_ACCESS = "TerminalAccess"
BRIDGEABLE_ENDPOINT_CARRIER_KINDS = {
ROUTE_CARRIER_KIND,
ROUTE_CARRIER_KIND_WIRE_DUCT,
ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END,
ROUTE_CARRIER_KIND_WIRING_CUT_OUT,
ROUTE_CARRIER_KIND_USER_PATH,
ROUTE_CARRIER_KIND_AUXILIARY_PATH,
}
MANAGED_ROUTE_SOURCE_KINDS = {
ROUTE_CARRIER_KIND_WIRE_DUCT,
ROUTE_CARRIER_KIND_WIRING_CUT_OUT,
@ -2707,7 +2715,7 @@ def build_route_graph(
bridged_segment_count = 0
blocked_bboxes = list(blocked_bboxes or [])
segments = []
wire_duct_endpoint_nodes = []
bridgeable_endpoint_nodes = []
def ensure_node(point):
key = _point_key(point, tolerance=tolerance)
@ -2762,10 +2770,11 @@ def build_route_graph(
if len(ordered) < 2:
continue
carrier = segment["carrier"]
if (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() == ROUTE_CARRIER_KIND_WIRE_DUCT:
carrier_kind = (getattr(carrier, "QetRouteCarrierKind", "") or "").strip() or ROUTE_CARRIER_KIND
if carrier_kind in BRIDGEABLE_ENDPOINT_CARRIER_KINDS:
for endpoint in (ordered[0], ordered[-1]):
endpoint_key = ensure_node(endpoint)
wire_duct_endpoint_nodes.append((endpoint_key, nodes[endpoint_key], carrier))
bridgeable_endpoint_nodes.append((endpoint_key, nodes[endpoint_key], carrier))
previous_key = ensure_node(ordered[0])
previous_point = nodes[previous_key]
for point in ordered[1:]:
@ -2787,9 +2796,9 @@ def build_route_graph(
adjoining_limit = max(float(adjoining_duct_tolerance or 0.0), 0.0)
bridged_pairs = set()
if adjoining_limit > tolerance:
for left_index, left in enumerate(wire_duct_endpoint_nodes):
for left_index, left in enumerate(bridgeable_endpoint_nodes):
left_key, left_point, left_carrier = left
for right_key, right_point, right_carrier in wire_duct_endpoint_nodes[left_index + 1:]:
for right_key, right_point, right_carrier in bridgeable_endpoint_nodes[left_index + 1:]:
if left_key == right_key or left_carrier is right_carrier:
continue
pair = tuple(sorted((left_key, right_key)))

@ -815,6 +815,34 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("network-dijkstra-v1", result["algorithm"])
self.assertEqual("Routed", result["route_status"])
def test_route_graph_bridges_adjoining_user_path_to_wire_duct_gap(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(50, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(60, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="UserPath",
)
network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0)
start_key, _start_distance = routing_network.nearest_node(network, app.Vector(0, 0, 20))
end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20))
result = routing_network.shortest_path_with_carriers(network, start_key, end_key)
self.assertEqual(1, network["bridged_segment_count"])
self.assertIsNotNone(result)
self.assertIn("UserPath", result["carrier_kinds"])
def test_auto_routing_respects_adjoining_duct_tolerance_option(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
@ -846,6 +874,39 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("Routed", result["route_status"])
self.assertEqual(1, result["network"]["bridged_segments"])
def test_auto_routing_uses_bridged_user_path_to_wire_duct_gap(self):
_install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0))
end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0))
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(50, 0, 20)],
project_uuid="project-1",
kind="WireDuct",
)
routing_network.create_route_carrier(
doc,
[app.Vector(60, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
kind="UserPath",
)
result = auto_routing.route_eplan_connection_between_terminals(
doc,
start,
end,
options={"adjoining_duct_tolerance": 15.0},
)
self.assertEqual("Routed", result["route_status"])
self.assertEqual(1, result["network"]["bridged_segments"])
self.assertIn("WireDuct", result["route_track"]["carrier_kinds"])
self.assertIn("UserPath", result["route_track"]["carrier_kinds"])
def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self):
_install_fake_freecad()
_terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
@ -1981,7 +2042,7 @@ class AutoRoutingTest(unittest.TestCase):
message = auto_routing.format_routing_path_network_report(diagnostic)
self.assertIn("桥接 1 段", message)
self.assertIn("桥接 1 段相邻主路径", message)
def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(self):
_install_fake_freecad()
@ -3203,7 +3264,7 @@ class AutoRoutingTest(unittest.TestCase):
message = auto_routing.format_eplan_connection_route_report(report)
self.assertIn("路径网络:自动桥接 1 段相邻线槽,避障屏蔽 2 段。", message)
self.assertIn("路径网络:自动桥接 1 段相邻主路径,避障屏蔽 2 段。", message)
def test_route_report_includes_parallel_lane_summary(self):
_install_fake_freecad()

Loading…
Cancel
Save