From f903badd22f478da285ac04cc3b22b34e00cfd8a Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sat, 30 May 2026 16:44:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96FreeCAD=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E5=85=A5=E7=BD=91=E7=82=B9=E6=8A=95?= =?UTF-8?q?=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 6 +- src/Mod/FreeCADExchange/RoutingNetwork.py | 85 +++++++++++++++++++ .../freecad_exchange_auto_routing_test.py | 68 ++++++++++++++- 3 files changed, 156 insertions(+), 3 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index bbc1eed..5ec44c7 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -629,8 +629,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non if network.get("segment_count", 0) <= 0: return None - start_key, start_distance = RoutingNetwork.nearest_node(network, start_exit) - end_key, end_distance = RoutingNetwork.nearest_node(network, end_exit) + start_key, start_distance, start_mode = RoutingNetwork.connect_point_to_network(network, start_exit) + end_key, end_distance, end_mode = RoutingNetwork.connect_point_to_network(network, end_exit) if start_key is None or end_key is None: return None @@ -677,6 +677,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non "nodes": len(network.get("nodes", {})), "entry_distance": float(start_distance or 0.0), "exit_distance": float(end_distance or 0.0), + "entry_point_mode": start_mode, + "exit_point_mode": end_mode, "obstacle_aware": bool(obstacle_aware), }, "route_track": path_result, diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 43862d7..fb38574 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2177,6 +2177,91 @@ def nearest_point_on_network(network, point): return nearest_node(network, target) +def connect_point_to_network(network, point): + """Connect the closest projected point to a route graph and return key/distance/mode.""" + if not isinstance(network, dict): + return None, None, "none" + nodes = network.get("nodes", {}) or {} + edges = network.get("edges", {}) or {} + if not nodes or not edges: + return None, None, "none" + + tolerance = float(network.get("tolerance", DEFAULT_NODE_TOLERANCE) or DEFAULT_NODE_TOLERANCE) + target = _vector(point) + best = None + seen = set() + for key, neighbors in edges.items(): + start = nodes.get(key) + if start is None: + continue + for next_key, _weight, carrier in neighbors: + pair = tuple(sorted((key, next_key))) + if pair in seen: + continue + seen.add(pair) + end = nodes.get(next_key) + if end is None: + continue + projected = _closest_point_on_segment(target, start, end) + distance = _distance(target, projected) + if best is None or distance < best["distance"]: + best = { + "key": key, + "next_key": next_key, + "carrier": carrier, + "point": projected, + "distance": distance, + } + + if best is None: + node_key, distance = nearest_node(network, target) + return node_key, distance, "node" if node_key is not None else "none" + + projected_key = _point_key(best["point"], tolerance=tolerance) + if projected_key in nodes: + return projected_key, best["distance"], "node" + + start_key = best["key"] + end_key = best["next_key"] + start = nodes[start_key] + end = nodes[end_key] + carrier = best["carrier"] + + def remove_edge_once(left_key, right_key, fallback_to_pair=False): + neighbors = list(edges.get(left_key, []) or []) + for index, (candidate_key, _weight, candidate_carrier) in enumerate(neighbors): + if candidate_key == right_key and candidate_carrier is carrier: + del neighbors[index] + edges[left_key] = neighbors + return True + if fallback_to_pair: + for index, (candidate_key, _weight, _candidate_carrier) in enumerate(neighbors): + if candidate_key == right_key: + del neighbors[index] + edges[left_key] = neighbors + return True + return False + + removed_forward = remove_edge_once(start_key, end_key) + remove_edge_once(end_key, start_key, fallback_to_pair=removed_forward) + + nodes[projected_key] = best["point"] + edges[projected_key] = [] + added_segments = 0 + for left_key, left_point, right_key, right_point in ( + (start_key, start, projected_key, best["point"]), + (projected_key, best["point"], end_key, end), + ): + weight = _distance(left_point, right_point) + if weight <= tolerance: + continue + edges[left_key].append((right_key, weight, carrier)) + edges[right_key].append((left_key, weight, carrier)) + added_segments += 1 + network["segment_count"] = max(int(network.get("segment_count", 0) or 0) - 1 + added_segments, 0) + return projected_key, best["distance"], "segment_projection" + + def _carrier_track_payload(carrier): return { "name": getattr(carrier, "Name", ""), diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index c4ae723..5a43b75 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -480,6 +480,45 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) + 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() + app = sys.modules["FreeCAD"] + doc = FakeDocument() + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(50, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + network = routing_network.build_route_graph(doc) + original_keys = set(network["nodes"].keys()) + bridge_keys = { + key + for key, point in network["nodes"].items() + if point.x in {50.0, 54.0} + } + + projected_key, _distance, mode = routing_network.connect_point_to_network(network, app.Vector(52, 0, 20)) + new_keys = set(network["nodes"].keys()) - original_keys + stale_bridge_edges = [ + (left_key, right_key) + for left_key, neighbors in network["edges"].items() + for right_key, _weight, _carrier in neighbors + if left_key in bridge_keys and right_key in bridge_keys + ] + + self.assertEqual("segment_projection", mode) + self.assertEqual(projected_key, next(iter(new_keys))) + self.assertEqual([], stale_bridge_edges) + self.assertEqual(4, network["segment_count"]) + def test_eplan_connection_route_prefers_wire_duct_over_auxiliary_range(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -719,6 +758,29 @@ class AutoRoutingTest(unittest.TestCase): end_point = access_carriers[0].Points[-1] self.assertEqual((50.0, 0.0, 20.0), (end_point.x, end_point.y, end_point.z)) + def test_eplan_connection_route_enters_network_at_segment_projection(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(50, 0, 0)) + end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(150, 0, 0)) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], + project_uuid="project-1", + kind="WireDuct", + ) + + result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) + + self.assertEqual("segment_projection", result["network"]["entry_point_mode"]) + self.assertEqual("segment_projection", result["network"]["exit_point_mode"]) + self.assertNotIn(0.0, [point.x for point in result["points"][1:-1]]) + self.assertNotIn(200.0, [point.x for point in result["points"][1:-1]]) + self.assertLess(result["length_mm"], 150.0) + def test_generate_routing_path_network_adds_wiring_cut_out_carrier(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() @@ -1383,7 +1445,11 @@ class AutoRoutingTest(unittest.TestCase): ], } - report = auto_routing.route_eplan_connections_from_payload(doc, payload) + report = auto_routing.route_eplan_connections_from_payload( + doc, + payload, + options={"avoid_obstacles": False}, + ) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["route_status_counts"]["Routed"])