fix: 修正FreeCAD自动布线诊断和替换逻辑

dev
Zhaowenlong 4 weeks ago
parent 0430826f82
commit 8a63e8de40

@ -765,9 +765,44 @@ def _bbox_payload(obj, clearance=0.0):
} }
def _collect_group_tree_ids(root):
excluded = set()
stack = [root]
while stack:
obj = stack.pop()
if obj is None or id(obj) in excluded:
continue
excluded.add(id(obj))
stack.extend(list(getattr(obj, "Group", []) or []))
return excluded
def _expanded_obstacle_exclusion_ids(doc, exclude):
excluded = set(id(obj) for obj in (exclude or []) if obj is not None)
endpoint_instance_ids = {
(getattr(obj, "QetInstanceId", "") or "").strip()
for obj in (exclude or [])
if obj is not None and (getattr(obj, "QetInstanceId", "") or "").strip()
}
if not endpoint_instance_ids:
return excluded
for obj in list(getattr(doc, "Objects", []) or []):
instance_id = (getattr(obj, "QetInstanceId", "") or "").strip()
parent_instance_ids = {
(getattr(parent, "QetInstanceId", "") or "").strip()
for parent in list(getattr(obj, "InList", []) or [])
if (getattr(parent, "QetInstanceId", "") or "").strip()
}
if instance_id in endpoint_instance_ids or parent_instance_ids.intersection(endpoint_instance_ids):
excluded.update(_collect_group_tree_ids(obj))
excluded.add(id(obj))
return excluded
def collect_obstacles(doc, exclude=None, options=None): def collect_obstacles(doc, exclude=None, options=None):
opts = _merged_options(options) opts = _merged_options(options)
excluded = set(id(obj) for obj in (exclude or []) if obj is not None) excluded = _expanded_obstacle_exclusion_ids(doc, exclude)
clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0) clearance = float(opts.get("obstacle_clearance", 0.0) or 0.0)
obstacles = [] obstacles = []
for obj in list(getattr(doc, "Objects", []) or []): for obj in list(getattr(doc, "Objects", []) or []):
@ -843,13 +878,34 @@ def detect_collisions(points, obstacles):
return collisions return collisions
def _detach_object_from_groups(doc, obj):
parents = list(getattr(obj, "InList", []) or [])
parents.extend(list(getattr(doc, "Objects", []) or []))
for parent in parents:
group = list(getattr(parent, "Group", []) or [])
if obj not in group:
continue
try:
if hasattr(parent, "removeObject"):
parent.removeObject(obj)
else:
parent.Group = [child for child in group if child is not obj]
except Exception:
try:
parent.Group = [child for child in group if child is not obj]
except Exception:
pass
def _remove_existing_auto_routes(doc, start_uuid, end_uuid, wire_uuid=""): def _remove_existing_auto_routes(doc, start_uuid, end_uuid, wire_uuid=""):
removed = 0 removed = 0
for obj in list(WiringObjects.iter_routed_wire_objects(doc)): for obj in list(WiringObjects.iter_routed_wire_objects(doc)):
if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested": if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested":
continue continue
if wire_uuid and (getattr(obj, "QetWireUuid", "") or "").strip() != wire_uuid: if wire_uuid:
if (getattr(obj, "QetWireUuid", "") or "").strip() != wire_uuid:
continue continue
else:
same_direction = ( same_direction = (
(getattr(obj, "QetStartTerminalUuid", "") or "").strip() == start_uuid (getattr(obj, "QetStartTerminalUuid", "") or "").strip() == start_uuid
and (getattr(obj, "QetEndTerminalUuid", "") or "").strip() == end_uuid and (getattr(obj, "QetEndTerminalUuid", "") or "").strip() == end_uuid
@ -861,6 +917,7 @@ def _remove_existing_auto_routes(doc, start_uuid, end_uuid, wire_uuid=""):
if not same_direction and not reverse_direction: if not same_direction and not reverse_direction:
continue continue
try: try:
_detach_object_from_groups(doc, obj)
doc.removeObject(obj.Name) doc.removeObject(obj.Name)
removed += 1 removed += 1
except Exception: except Exception:
@ -1076,7 +1133,7 @@ def format_terminal_binding_report(report):
return message return message
def route_all_from_payload(doc, payload, options=None): def route_all_from_payload(doc, payload, options=None, prepared_layout=None):
if doc is None: if doc is None:
raise AutoRoutingError("No FreeCAD document is available.") raise AutoRoutingError("No FreeCAD document is available.")
if not isinstance(payload, dict): if not isinstance(payload, dict):
@ -1107,6 +1164,8 @@ def route_all_from_payload(doc, payload, options=None):
"errors": [], "errors": [],
"routes": [], "routes": [],
} }
if isinstance(prepared_layout, dict):
report["prepared_layout"] = prepared_layout
missing_endpoint_uuids = set() missing_endpoint_uuids = set()
for index, item in enumerate(wires): for index, item in enumerate(wires):
@ -1256,11 +1315,17 @@ def _clear_auto_route_batch_diagnostics(doc):
def _write_auto_route_batch_diagnostic(doc, report): def _write_auto_route_batch_diagnostic(doc, report):
if doc is None or not isinstance(report, dict): if doc is None or not isinstance(report, dict):
return None return None
if not report.get("errors") and not report.get("missing_endpoint_uuids") and report.get("collision_warnings", 0) <= 0:
return None
project_uuid = _project_uuid(doc) project_uuid = _project_uuid(doc)
group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) group = WiringObjects.ensure_diagnostic_group(doc, project_uuid)
_clear_auto_route_batch_diagnostics(doc) _clear_auto_route_batch_diagnostics(doc)
if (
report.get("total_wires", 0) <= 0
and not report.get("routes")
and not report.get("errors")
and not report.get("missing_endpoint_uuids")
and report.get("collision_warnings", 0) <= 0
):
return None
diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETAutoRouteDiagnostic")) diagnostic = doc.addObject("App::DocumentObjectGroup", _unique_name(doc, "QETAutoRouteDiagnostic"))
diagnostic.Label = "QET Auto Route Diagnostic" diagnostic.Label = "QET Auto Route Diagnostic"
_set_string(diagnostic, "QetDiagnosticKind", "AutoRouteBatch", "QET diagnostic kind") _set_string(diagnostic, "QetDiagnosticKind", "AutoRouteBatch", "QET diagnostic kind")
@ -1317,9 +1382,9 @@ def bind_wire_task_terminals_from_tasks(doc):
return bind_wire_task_terminals_from_payload(doc, _wire_tasks_payload(doc)) return bind_wire_task_terminals_from_payload(doc, _wire_tasks_payload(doc))
def route_all_tasks(doc, options=None): def route_all_tasks(doc, options=None, prepared_layout=None):
payload = _wire_tasks_payload(doc) payload = _wire_tasks_payload(doc)
return route_all_from_payload(doc, payload, options=options) return route_all_from_payload(doc, payload, options=options, prepared_layout=prepared_layout)
def prepare_eplan_style_layout(doc, project_uuid="", options=None): def prepare_eplan_style_layout(doc, project_uuid="", options=None):
@ -1356,6 +1421,7 @@ def clear_auto_routes(doc):
if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested": if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested":
continue continue
try: try:
_detach_object_from_groups(doc, obj)
doc.removeObject(obj.Name) doc.removeObject(obj.Name)
removed += 1 removed += 1
except Exception: except Exception:
@ -1398,10 +1464,9 @@ class CommandAutoRouteAll:
payload = getattr(App, "_qet_exchange_payload", None) payload = getattr(App, "_qet_exchange_payload", None)
if isinstance(payload, dict) and payload.get("wires"): if isinstance(payload, dict) and payload.get("wires"):
report = route_all_from_payload(doc, payload) report = route_all_from_payload(doc, payload, prepared_layout=prepared_layout)
else: else:
report = route_all_tasks(doc) report = route_all_tasks(doc, prepared_layout=prepared_layout)
report["prepared_layout"] = prepared_layout
if report.get("total_wires", 0) <= 0: if report.get("total_wires", 0) <= 0:
_console_error("没有导线任务。一键自动布线需要 QET wires[] 或 QETWiring_01_Tasks。") _console_error("没有导线任务。一键自动布线需要 QET wires[] 或 QETWiring_01_Tasks。")
return return

@ -146,10 +146,9 @@ class AutoRoutingController:
) )
payload = getattr(App, "_qet_exchange_payload", None) payload = getattr(App, "_qet_exchange_payload", None)
if isinstance(payload, dict) and payload.get("wires"): if isinstance(payload, dict) and payload.get("wires"):
report = AutoRouting.route_all_from_payload(doc, payload) report = AutoRouting.route_all_from_payload(doc, payload, prepared_layout=prepared_layout)
else: else:
report = AutoRouting.route_all_tasks(doc) report = AutoRouting.route_all_tasks(doc, prepared_layout=prepared_layout)
report["prepared_layout"] = prepared_layout
if report.get("total_wires", 0) <= 0: if report.get("total_wires", 0) <= 0:
raise AutoRoutingPanelError( raise AutoRoutingPanelError(
"没有导线任务。请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中存在导线任务。" "没有导线任务。请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中存在导线任务。"

@ -341,6 +341,36 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("y", payload["lane"]["axis"]) self.assertEqual("y", payload["lane"]["axis"])
self.assertEqual(12.0, payload["lane"]["offset_mm"]) self.assertEqual(12.0, payload["lane"]["offset_mm"])
def test_auto_route_replaces_existing_wire_uuid_when_endpoints_change(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_old = _terminal(doc, terminal_objects, "TerminalStartOld", "terminal-start-old", app.Vector(0, 0, 0))
end_old = _terminal(doc, terminal_objects, "TerminalEndOld", "terminal-end-old", app.Vector(100, 0, 0))
start_new = _terminal(doc, terminal_objects, "TerminalStartNew", "terminal-start-new", app.Vector(0, 40, 0))
end_new = _terminal(doc, terminal_objects, "TerminalEndNew", "terminal-end-new", app.Vector(100, 40, 0))
routing_network.create_route_carrier(
doc,
[
app.Vector(0, 0, 20),
app.Vector(100, 0, 20),
app.Vector(100, 40, 20),
app.Vector(0, 40, 20),
],
project_uuid="project-1",
kind="WireDuct",
)
auto_routing.route_between_terminals(doc, start_old, end_old, wire_uuid="wire-1")
auto_routing.route_between_terminals(doc, start_new, end_new, wire_uuid="wire-1")
routed_wires = list(wiring_objects.iter_routed_wire_objects(doc))
self.assertEqual(1, len(routed_wires))
self.assertEqual("terminal-start-new", routed_wires[0].QetStartTerminalUuid)
self.assertEqual("terminal-end-new", routed_wires[0].QetEndTerminalUuid)
def test_route_carrier_styles_make_generated_objects_distinguishable(self): def test_route_carrier_styles_make_generated_objects_distinguishable(self):
_install_fake_freecad() _install_fake_freecad()
terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules()
@ -691,6 +721,12 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual(1, report["routed"]) self.assertEqual(1, report["routed"])
self.assertEqual(1, report["prepared_layout"]["wire_duct_carriers"]) self.assertEqual(1, report["prepared_layout"]["wire_duct_carriers"])
self.assertEqual(2, report["prepared_layout"]["terminal_access_carriers"]) self.assertEqual(2, report["prepared_layout"]["terminal_access_carriers"])
diagnostic_group = doc.getObject("QETWiring_05_Diagnostics")
self.assertIsNotNone(diagnostic_group)
self.assertEqual(1, len(diagnostic_group.Group))
diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson)
self.assertEqual(1, diagnostic_payload["prepared_layout"]["wire_duct_carriers"])
self.assertEqual(2, diagnostic_payload["prepared_layout"]["terminal_access_carriers"])
def test_auto_route_rejects_far_network_entry_to_avoid_huge_render_bbox(self): def test_auto_route_rejects_far_network_entry_to_avoid_huge_render_bbox(self):
_install_fake_freecad() _install_fake_freecad()
@ -940,6 +976,31 @@ class AutoRoutingTest(unittest.TestCase):
self.assertEqual("CollisionWarning", result["wire"].RouteStatus) self.assertEqual("CollisionWarning", result["wire"].RouteStatus)
self.assertEqual(1, result["collision_count"]) self.assertEqual(1, 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()
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))
device = doc.addObject("App::DocumentObjectGroup", "QETDeviceStart")
device.QetInstanceId = start.QetInstanceId
device.addObject(start)
body = doc.addObject("Part::Feature", "StartDeviceBody")
body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 15))
device.addObject(body)
routing_network.create_route_carrier(
doc,
[app.Vector(0, 0, 20), app.Vector(100, 0, 20)],
project_uuid="project-1",
)
result = auto_routing.route_between_terminals(doc, start, end)
self.assertEqual("Routed", result["route_status"])
self.assertEqual(0, result["collision_count"])
def test_route_all_from_payload_skips_missing_terminal(self): def test_route_all_from_payload_skips_missing_terminal(self):
_install_fake_freecad() _install_fake_freecad()
terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()

Loading…
Cancel
Save