fix: 降低FreeCAD自动布线碰撞误报

dev
Zhaowenlong 4 weeks ago
parent 33ae1d8e31
commit bb30eec80b

@ -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:

@ -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()

Loading…
Cancel
Save