diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index bfba5e7..6dff20c 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -46,6 +46,8 @@ DEFAULT_OPTIONS = { "allow_floating_fallback": False, # 障碍包围盒会按这个距离膨胀,用于提前发现贴碰风险。 "obstacle_clearance": 5.0, + # 端子出线/入线段通常会贴近端子塑壳或设备外壳,不作为主路径碰撞判定依据。 + "ignore_endpoint_collision_segments": True, # 防止坐标异常或端子离路由网络过远时生成超长接入线,把 FreeCAD # 视图包围盒拉得过大,导致旋转时模型被裁剪到看不见。 "terminal_access_max_distance": 1000.0, @@ -800,10 +802,32 @@ def _expanded_obstacle_exclusion_ids(doc, exclude): return excluded +def _distance_point_to_bbox(point, bbox): + squared = 0.0 + for axis, min_key, max_key in ( + ("x", "xmin", "xmax"), + ("y", "ymin", "ymax"), + ("z", "zmin", "zmax"), + ): + value = _axis_value(point, axis) + low = float(bbox[min_key]) + high = float(bbox[max_key]) + if value < low: + squared += (low - value) * (low - value) + elif value > high: + squared += (value - high) * (value - high) + return math.sqrt(squared) + + def collect_obstacles(doc, exclude=None, options=None): opts = _merged_options(options) excluded = _expanded_obstacle_exclusion_ids(doc, exclude) clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0) + endpoint_clearance = max(float(opts.get("terminal_exit_length", 0.0) or 0.0), 0.0) + clearance + endpoint_points = [] + for obj in exclude or []: + if obj is not None and TerminalObjects.is_terminal_object(obj): + endpoint_points.append(_terminal_origin(obj)) obstacles = [] for obj in list(getattr(doc, "Objects", []) or []): if id(obj) in excluded: @@ -820,6 +844,11 @@ def collect_obstacles(doc, exclude=None, options=None): bbox = _bbox_payload(obj, clearance=clearance) if bbox is None: continue + if endpoint_points and any( + _distance_point_to_bbox(point, bbox) <= endpoint_clearance + for point in endpoint_points + ): + continue obstacles.append( { "name": getattr(obj, "Name", ""), @@ -861,9 +890,12 @@ def _segment_intersects_bbox(start, end, bbox): return True -def detect_collisions(points, obstacles): +def detect_collisions(points, obstacles, ignored_segment_indices=None): + ignored = set(ignored_segment_indices or []) collisions = [] for index in range(max(len(points) - 1, 0)): + if index in ignored: + continue start = points[index] end = points[index + 1] for obstacle in obstacles: @@ -878,6 +910,16 @@ def detect_collisions(points, obstacles): return collisions +def _endpoint_collision_segment_indices(points): + segment_count = max(len(points or []) - 1, 0) + if segment_count <= 0: + return set() + ignored = {0} + if segment_count > 1: + ignored.add(segment_count - 1) + return ignored + + def _detach_object_from_groups(doc, obj): parents = list(getattr(obj, "InList", []) or []) parents.extend(list(getattr(doc, "Objects", []) or [])) @@ -1026,7 +1068,10 @@ def route_between_terminals( raise AutoRoutingError("Auto-routing produced fewer than two points.") obstacles = collect_obstacles(doc, exclude=[start_terminal, end_terminal], options=opts) - collisions = detect_collisions(points, obstacles) + ignored_collision_segments = set() + if opts.get("ignore_endpoint_collision_segments", True): + ignored_collision_segments = _endpoint_collision_segment_indices(points) + collisions = detect_collisions(points, obstacles, ignored_segment_indices=ignored_collision_segments) status = "CollisionWarning" if collisions else "Routed" wire_name = _unique_name(doc, _wire_object_name(start_terminal, end_terminal, wire_uuid)) @@ -1161,6 +1206,7 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): "skipped_invalid": 0, "missing_endpoint_uuids": [], "missing_endpoint_samples": [], + "collision_samples": [], "errors": [], "routes": [], } @@ -1218,6 +1264,20 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): continue if result["route_status"] == "CollisionWarning": report["collision_warnings"] += 1 + route_collision_samples = [] + for collision in list(result.get("collisions", []) or [])[:3]: + sample = dict(collision) + sample.update( + { + "wire_uuid": _wire_item_value(item, "wire_id", "wire_uuid", "id"), + "wire_label": _wire_item_value(item, "wire_label", "wire_mark"), + "start_terminal_uuid": start_uuid, + "end_terminal_uuid": end_uuid, + } + ) + route_collision_samples.append(sample) + if len(report["collision_samples"]) < 8: + report["collision_samples"].append(sample) report["routed"] += 1 route_length = float(result.get("length_mm", 0.0) or 0.0) report["total_length_mm"] += route_length @@ -1234,6 +1294,7 @@ def route_all_from_payload(doc, payload, options=None, prepared_layout=None): "lane": result.get("lane", {}), "network": result.get("network", {}), "collision_count": result["collision_count"], + "collision_samples": route_collision_samples, } ) report["missing_endpoint_uuids"] = sorted(missing_endpoint_uuids) @@ -1260,6 +1321,19 @@ def format_route_all_report(report): errors = report.get("errors", []) or [] if errors: message += "\n首个错误:{0}".format(str(errors[0])) + collision_sample = (report.get("collision_samples") or [None])[0] + if collision_sample: + obstacle_text = ( + collision_sample.get("obstacle_label") + or collision_sample.get("obstacle_name") + or "未知对象" + ) + message += "\n碰撞示例:导线 {0} 碰到 {1}。".format( + collision_sample.get("wire_label") + or collision_sample.get("wire_uuid") + or "未知导线", + obstacle_text, + ) auto_bound = report.get("auto_bound_terminals", 0) auto_created = report.get("auto_created_terminals", 0) if auto_bound or auto_created: diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index b218a3d..37b6d37 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -976,6 +976,27 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("CollisionWarning", result["wire"].RouteStatus) self.assertEqual(1, result["collision_count"]) + def test_auto_route_ignores_terminal_exit_segment_collision(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", + ) + terminal_body = doc.addObject("Part::Feature", "UngroupedTerminalBody") + terminal_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15)) + + result = auto_routing.route_between_terminals(doc, start, end) + + self.assertEqual("Routed", result["route_status"]) + self.assertEqual(0, result["collision_count"]) + def test_auto_route_ignores_endpoint_device_body_as_obstacle(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() @@ -1132,6 +1153,45 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("network-dijkstra-v1", route["algorithm"]) self.assertEqual(1, route["network"]["carriers"]) + def test_route_all_report_includes_collision_samples(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, "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, 100), app.Vector(100, 0, 100)], + project_uuid="project-1", + ) + obstacle = doc.addObject("Part::Feature", "MiddleObstacle") + obstacle.Label = "Middle Obstacle" + obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) + payload = { + "project_uuid": "project-1", + "wires": [ + { + "wire_id": "wire-1", + "wire_label": "N4111", + "start_terminal_uuid": "terminal-start", + "end_terminal_uuid": "terminal-end", + } + ], + } + + report = auto_routing.route_all_from_payload(doc, payload) + message = auto_routing.format_route_all_report(report) + + self.assertEqual(1, report["collision_warnings"]) + self.assertEqual("wire-1", report["collision_samples"][0]["wire_uuid"]) + self.assertEqual("N4111", report["collision_samples"][0]["wire_label"]) + self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) + self.assertEqual("Middle Obstacle", report["routes"][0]["collision_samples"][0]["obstacle_label"]) + self.assertIn("碰撞示例", message) + self.assertIn("Middle Obstacle", message) + def test_route_all_report_calls_out_local_unbound_terminals(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()