diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index d87e7ce..46622ec 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -25,6 +25,7 @@ DEFAULT_OPTIONS = { "terminal_exit_length": 20.0, "lane_axis": "auto", "lane_spacing": 10.0, + "segment_reuse_penalty": 200.0, # 线槽网络相关参数。 "use_routing_network": True, "network_entry_max_distance": 1000.0, @@ -791,6 +792,8 @@ def build_network_route(start_terminal, end_terminal, route_index=0, options=Non end_key, bend_penalty=float(opts.get("bend_penalty", 0.0) or 0.0), kind_cost_factors=opts.get("carrier_kind_cost_factors", {}), + segment_usage_costs=opts.get("segment_usage_costs", {}), + segment_reuse_penalty=float(opts.get("segment_reuse_penalty", 0.0) or 0.0), ) path_keys = path_result.get("path", []) if isinstance(path_result, dict) else [] if not path_keys: @@ -1424,18 +1427,22 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la missing_endpoint_uuids = set() lane_indexes_by_pair = {} lane_indexes_by_segment = {} + segment_usage_costs = {} def add_status(status): key = str(status or "").strip() or "Unknown" report["route_status_counts"][key] = report["route_status_counts"].get(key, 0) + 1 def create_route(route_lane_index, item, start_terminal, end_terminal, endpoint_metadata): + route_options = dict(options or {}) + if isinstance(item, dict) and "__segment_usage_costs" in item: + route_options["segment_usage_costs"] = item.get("__segment_usage_costs", {}) return route_eplan_connection_between_terminals( doc, start_terminal, end_terminal, route_index=route_lane_index, - options=options, + options=route_options, wire_uuid=_wire_item_value(item, "wire_id", "wire_uuid", "id"), wire_label=_wire_item_value(item, "wire_label", "wire_mark"), net_uuid=_wire_item_value(item, "net_uuid"), @@ -1492,7 +1499,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la } result = create_route( route_lane_index, - item, + dict(item, __segment_usage_costs=segment_usage_costs), start_terminal, end_terminal, endpoint_metadata, @@ -1507,7 +1514,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la try: result = create_route( final_lane_index, - item, + dict(item, __segment_usage_costs=segment_usage_costs), start_terminal, end_terminal, endpoint_metadata, @@ -1548,6 +1555,7 @@ def route_eplan_connections_from_payload(doc, payload, options=None, prepared_la lane_indexes_by_segment.get(segment_key, 0), int(result.get("lane", {}).get("index", 0) or 0) + 1, ) + segment_usage_costs[segment_key] = segment_usage_costs.get(segment_key, 0) + 1 if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 add_status(result["route_status"]) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index fb38574..5ac5f4a 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -2270,7 +2270,23 @@ def _carrier_track_payload(carrier): } -def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, kind_cost_factors=None): +def _segment_usage_key(carrier, from_key, to_key): + carrier_name = getattr(carrier, "Name", "") if carrier is not None else "" + return ( + carrier_name, + tuple(sorted((from_key, to_key))), + ) + + +def shortest_path_with_carriers( + network, + start_key, + end_key, + bend_penalty=0.0, + kind_cost_factors=None, + segment_usage_costs=None, + segment_reuse_penalty=0.0, +): """Dijkstra search with a small extra cost when route direction changes.""" if start_key is None or end_key is None: return None @@ -2343,8 +2359,17 @@ def shortest_path_with_carriers(network, start_key, end_key, bend_penalty=0.0, k bend_cost = 0.0 if previous_direction is not None and direction != previous_direction: bend_cost = float(bend_penalty or 0.0) + usage_cost = 0.0 + if segment_usage_costs: + usage_count = float(segment_usage_costs.get(_segment_usage_key(carrier, key, next_key), 0.0) or 0.0) + usage_cost = usage_count * float(segment_reuse_penalty or 0.0) next_state = (next_key, direction) - next_cost = cost + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) + bend_cost + next_cost = ( + cost + + float(weight) * _carrier_cost_factor(carrier, kind_cost_factors) + + bend_cost + + usage_cost + ) if next_cost < distances.get(next_state, float("inf")): distances[next_state] = next_cost previous[next_state] = { diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 0c86ca8..3787c85 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -1967,6 +1967,74 @@ class AutoRoutingTest(unittest.TestCase): self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) + def test_route_eplan_connections_prefers_unused_alternate_route_segments(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") + _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)], + label="Direct Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], + label="Left Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], + label="Alternate Duct", + project_uuid="project-1", + kind="WireDuct", + ) + routing_network.create_route_carrier( + doc, + [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], + label="Right Bridge", + project_uuid="project-1", + kind="WireDuct", + ) + 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", + }, + ], + } + + report = auto_routing.route_eplan_connections_from_payload(doc, payload) + + first_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][0]["route_track"]["segments"] + ] + second_labels = [ + segment["carrier"]["label"] + for segment in report["routes"][1]["route_track"]["segments"] + ] + self.assertIn("Direct Duct", first_labels) + self.assertIn("Alternate Duct", second_labels) + self.assertNotIn("Direct Duct", second_labels) + def test_route_eplan_connections_report_includes_collision_samples(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules()