From 8a63e8de40100e91b49a3e0d6b0afcdc798ebcbe Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Fri, 29 May 2026 09:41:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3FreeCAD=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B8=83=E7=BA=BF=E8=AF=8A=E6=96=AD=E5=92=8C=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/AutoRouting.py | 107 ++++++++++++++---- src/Mod/FreeCADExchange/AutoRoutingPanel.py | 5 +- .../freecad_exchange_auto_routing_test.py | 61 ++++++++++ 3 files changed, 149 insertions(+), 24 deletions(-) diff --git a/src/Mod/FreeCADExchange/AutoRouting.py b/src/Mod/FreeCADExchange/AutoRouting.py index 5ae7e55..bfba5e7 100644 --- a/src/Mod/FreeCADExchange/AutoRouting.py +++ b/src/Mod/FreeCADExchange/AutoRouting.py @@ -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): 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) obstacles = [] for obj in list(getattr(doc, "Objects", []) or []): @@ -843,24 +878,46 @@ def detect_collisions(points, obstacles): 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=""): removed = 0 for obj in list(WiringObjects.iter_routed_wire_objects(doc)): if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested": continue - if wire_uuid and (getattr(obj, "QetWireUuid", "") or "").strip() != wire_uuid: - continue - same_direction = ( - (getattr(obj, "QetStartTerminalUuid", "") or "").strip() == start_uuid - and (getattr(obj, "QetEndTerminalUuid", "") or "").strip() == end_uuid - ) - reverse_direction = ( - (getattr(obj, "QetStartTerminalUuid", "") or "").strip() == end_uuid - and (getattr(obj, "QetEndTerminalUuid", "") or "").strip() == start_uuid - ) - if not same_direction and not reverse_direction: - continue + if wire_uuid: + if (getattr(obj, "QetWireUuid", "") or "").strip() != wire_uuid: + continue + else: + same_direction = ( + (getattr(obj, "QetStartTerminalUuid", "") or "").strip() == start_uuid + and (getattr(obj, "QetEndTerminalUuid", "") or "").strip() == end_uuid + ) + reverse_direction = ( + (getattr(obj, "QetStartTerminalUuid", "") or "").strip() == end_uuid + and (getattr(obj, "QetEndTerminalUuid", "") or "").strip() == start_uuid + ) + if not same_direction and not reverse_direction: + continue try: + _detach_object_from_groups(doc, obj) doc.removeObject(obj.Name) removed += 1 except Exception: @@ -1076,7 +1133,7 @@ def format_terminal_binding_report(report): 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: raise AutoRoutingError("No FreeCAD document is available.") if not isinstance(payload, dict): @@ -1107,6 +1164,8 @@ def route_all_from_payload(doc, payload, options=None): "errors": [], "routes": [], } + if isinstance(prepared_layout, dict): + report["prepared_layout"] = prepared_layout missing_endpoint_uuids = set() 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): if doc is None or not isinstance(report, dict): 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) group = WiringObjects.ensure_diagnostic_group(doc, project_uuid) _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.Label = "QET Auto Route Diagnostic" _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)) -def route_all_tasks(doc, options=None): +def route_all_tasks(doc, options=None, prepared_layout=None): 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): @@ -1356,6 +1421,7 @@ def clear_auto_routes(doc): if (getattr(obj, "RouteType", "") or "").strip() != "AutoSuggested": continue try: + _detach_object_from_groups(doc, obj) doc.removeObject(obj.Name) removed += 1 except Exception: @@ -1398,10 +1464,9 @@ class CommandAutoRouteAll: payload = getattr(App, "_qet_exchange_payload", None) 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: - report = route_all_tasks(doc) - report["prepared_layout"] = prepared_layout + report = route_all_tasks(doc, prepared_layout=prepared_layout) if report.get("total_wires", 0) <= 0: _console_error("没有导线任务。一键自动布线需要 QET wires[] 或 QETWiring_01_Tasks。") return diff --git a/src/Mod/FreeCADExchange/AutoRoutingPanel.py b/src/Mod/FreeCADExchange/AutoRoutingPanel.py index 72536fc..3824124 100644 --- a/src/Mod/FreeCADExchange/AutoRoutingPanel.py +++ b/src/Mod/FreeCADExchange/AutoRoutingPanel.py @@ -146,10 +146,9 @@ class AutoRoutingController: ) payload = getattr(App, "_qet_exchange_payload", None) 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: - report = AutoRouting.route_all_tasks(doc) - report["prepared_layout"] = prepared_layout + report = AutoRouting.route_all_tasks(doc, prepared_layout=prepared_layout) if report.get("total_wires", 0) <= 0: raise AutoRoutingPanelError( "没有导线任务。请先从 QET 导入 wires[],或确认 QETWiring_01_Tasks 中存在导线任务。" diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 79ef112..b218a3d 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -341,6 +341,36 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("y", payload["lane"]["axis"]) 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): _install_fake_freecad() 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["prepared_layout"]["wire_duct_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): _install_fake_freecad() @@ -940,6 +976,31 @@ class AutoRoutingTest(unittest.TestCase): self.assertEqual("CollisionWarning", result["wire"].RouteStatus) 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): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules()