From ba9d971ef50a4d6dd9a9cb41238a9cc23dbcd641 Mon Sep 17 00:00:00 2001 From: Zhaowenlong Date: Sun, 31 May 2026 16:52:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=B7=E6=96=B0=E7=BA=BF=E6=A7=BD?= =?UTF-8?q?=E6=BA=90=E5=AF=B9=E5=BA=94=E5=B8=83=E7=BA=BF=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Mod/FreeCADExchange/RoutingNetwork.py | 204 ++++++++++++++---- .../freecad_exchange_auto_routing_test.py | 32 +++ 2 files changed, 193 insertions(+), 43 deletions(-) diff --git a/src/Mod/FreeCADExchange/RoutingNetwork.py b/src/Mod/FreeCADExchange/RoutingNetwork.py index 0a27f19..1bc4109 100644 --- a/src/Mod/FreeCADExchange/RoutingNetwork.py +++ b/src/Mod/FreeCADExchange/RoutingNetwork.py @@ -588,11 +588,7 @@ def _create_carrier_geometry(doc, name, points): return obj -def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): - """Create a routable carrier from ordered 3D points.""" - if doc is None: - raise RoutingNetworkError("No FreeCAD document is available.") - +def _normalized_route_points(points): normalized = [] for point in points or []: vector = _vector(point) @@ -600,19 +596,47 @@ def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARR continue if not normalized or _distance(normalized[-1], vector) > DEFAULT_NODE_TOLERANCE: normalized.append(vector) + return normalized - if len(normalized) < 2: - raise RoutingNetworkError("A route carrier requires at least two distinct points.") - name = _unique_name(doc, "QETRouteCarrier") - carrier = _create_carrier_geometry(doc, name, normalized) - carrier.Label = label or "QET Route Carrier" +def _set_route_carrier_points(carrier, points): _ensure_vector_list_property( carrier, "Points", "Ordered centerline points used by the 3D router", ) - carrier.Points = list(normalized) + carrier.Points = list(points) + try: + import Part + + carrier.Shape = Part.makePolygon(points) + except Exception: + pass + + +def _update_route_carrier(carrier, points, project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): + normalized = _normalized_route_points(points) + if len(normalized) < 2: + return False + _set_route_carrier_points(carrier, normalized) + _set_route_carrier_semantics(carrier, project_uuid=project_uuid, kind=kind, capacity=capacity) + _style_route_carrier(carrier, kind) + return True + + +def create_route_carrier(doc, points, label="", project_uuid="", kind=ROUTE_CARRIER_KIND, capacity=1): + """Create a routable carrier from ordered 3D points.""" + if doc is None: + raise RoutingNetworkError("No FreeCAD document is available.") + + normalized = _normalized_route_points(points) + if len(normalized) < 2: + raise RoutingNetworkError("A route carrier requires at least two distinct points.") + + name = _unique_name(doc, "QETRouteCarrier") + carrier = _create_carrier_geometry(doc, name, normalized) + carrier.Label = label or "QET Route Carrier" + _set_route_carrier_points(carrier, normalized) _set_route_carrier_semantics(carrier, project_uuid=project_uuid, kind=kind, capacity=capacity) group = WiringObjects.ensure_carrier_group(doc, project_uuid) @@ -1396,6 +1420,40 @@ def _wire_duct_centerline_from_bbox(bbox, margin=DEFAULT_WIRE_DUCT_MARGIN, min_a ).get("centerline", []) +def _sync_wire_duct_source_carriers(doc, source, spec, project_uuid="", capacity=1): + carriers = _live_source_carriers(doc, source) + if not carriers: + return False + + desired = [ + (spec.get("centerline", []), ROUTE_CARRIER_KIND_WIRE_DUCT), + ] + desired.extend( + (points, ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END) + for points in (spec.get("open_ends", []) or []) + ) + + updated = [] + for carrier, desired_item in zip(carriers, desired): + points, kind = desired_item + if _update_route_carrier( + carrier, + points, + project_uuid=project_uuid, + kind=kind, + capacity=capacity, + ): + updated.append(carrier) + + if updated: + _mark_wire_duct_source(source, updated[0], updated) + try: + doc.recompute() + except Exception: + pass + return True + + def _wiring_cut_out_points_from_bbox(bbox): extents = _bbox_extents(bbox) if not extents: @@ -1436,7 +1494,57 @@ def _wire_duct_sources_from_selection(selection_ex): return sources -def _mark_wire_duct_source(source, carrier): +def _route_source_carrier_names(source): + names = [] + try: + raw = (getattr(source, "QetRouteCarrierNamesJson", "") or "").strip() + if raw: + parsed = json.loads(raw) + if isinstance(parsed, list): + names.extend(str(item).strip() for item in parsed if str(item).strip()) + except Exception: + names = [] + carrier_name = (getattr(source, "QetRouteCarrierName", "") or "").strip() + if carrier_name: + names.insert(0, carrier_name) + result = [] + seen = set() + for name in names: + if name in seen: + continue + seen.add(name) + result.append(name) + return result + + +def _live_source_carriers(doc, source): + if doc is None or source is None: + return [] + carriers = [] + for carrier_name in _route_source_carrier_names(source): + carrier = doc.getObject(carrier_name) + if carrier is not None and is_route_carrier(carrier): + carriers.append(carrier) + return carriers + + +def _remember_source_carriers(source, carriers): + live_names = [ + getattr(carrier, "Name", "") + for carrier in (carriers or []) + if carrier is not None and getattr(carrier, "Name", "") + ] + if live_names: + TerminalObjects.ensure_string_property( + source, + "QetRouteCarrierNamesJson", + PROPERTY_GROUP, + "Generated route carriers for this source", + json.dumps(live_names, ensure_ascii=False), + ) + + +def _mark_wire_duct_source(source, carrier, carriers=None): if source is None: return try: @@ -1449,6 +1557,7 @@ def _mark_wire_duct_source(source, carrier): "Generated route carrier for this source", getattr(carrier, "Name", ""), ) + _remember_source_carriers(source, carriers or ([carrier] if carrier is not None else [])) except Exception: pass @@ -1508,13 +1617,8 @@ def _mark_terminal_access_source(source, carrier): def _live_source_carrier(doc, source): - carrier_name = (getattr(source, "QetRouteCarrierName", "") or "").strip() - if not carrier_name or doc is None: - return None - carrier = doc.getObject(carrier_name) - if carrier is not None and is_route_carrier(carrier): - return carrier - return None + carriers = _live_source_carriers(doc, source) + return carriers[0] if carriers else None def detect_wire_duct_sources(doc, min_aspect=DEFAULT_AUTO_WIRE_DUCT_MIN_ASPECT): @@ -1610,8 +1714,6 @@ def create_wire_duct_carriers_from_document( """Auto-detect wire duct objects in the document and create WireDuct centerlines.""" created = [] for index, source in enumerate(detect_wire_duct_sources(doc, min_aspect=min_aspect), start=1): - if _live_source_carrier(doc, source) is not None: - continue bbox = _bound_box_from_object(source) if bbox is None: continue @@ -1625,6 +1727,14 @@ def create_wire_duct_carriers_from_document( continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" capacity = _route_carrier_capacity_value(source, default=1) + if _sync_wire_duct_source_carriers( + doc, + source, + spec, + project_uuid=project_uuid, + capacity=capacity, + ): + continue carrier = create_route_carrier( doc, points, @@ -1633,21 +1743,22 @@ def create_wire_duct_carriers_from_document( kind=ROUTE_CARRIER_KIND_WIRE_DUCT, capacity=capacity, ) - _mark_wire_duct_source(source, carrier) + source_created = [carrier] created.append(carrier) for end_index, open_end_points in enumerate(spec.get("open_ends", []) or [], start=1): if len(open_end_points) < 2: continue - created.append( - create_route_carrier( - doc, - open_end_points, - label="QET Auto Wire Duct Open End {0} {1}".format(label, end_index), - project_uuid=project_uuid, - kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, - capacity=capacity, - ) + open_end_carrier = create_route_carrier( + doc, + open_end_points, + label="QET Auto Wire Duct Open End {0} {1}".format(label, end_index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + capacity=capacity, ) + source_created.append(open_end_carrier) + created.append(open_end_carrier) + _mark_wire_duct_source(source, carrier, source_created) return created @@ -1916,8 +2027,6 @@ def create_wire_duct_carriers_from_selection( """Create WireDuct centerline carriers from selected duct-like solids.""" created = [] for index, source in enumerate(_wire_duct_sources_from_selection(selection_ex), start=1): - if _live_source_carrier(doc, source) is not None: - continue bbox = _bound_box_from_object(source) if bbox is None: continue @@ -1931,6 +2040,14 @@ def create_wire_duct_carriers_from_selection( continue label = getattr(source, "Label", "") or getattr(source, "Name", "") or "Wire Duct" capacity = _route_carrier_capacity_value(source, default=1) + if _sync_wire_duct_source_carriers( + doc, + source, + spec, + project_uuid=project_uuid, + capacity=capacity, + ): + continue carrier = create_route_carrier( doc, points, @@ -1939,21 +2056,22 @@ def create_wire_duct_carriers_from_selection( kind=ROUTE_CARRIER_KIND_WIRE_DUCT, capacity=capacity, ) - _mark_wire_duct_source(source, carrier) + source_created = [carrier] created.append(carrier) for end_index, open_end_points in enumerate(spec.get("open_ends", []) or [], start=1): if len(open_end_points) < 2: continue - created.append( - create_route_carrier( - doc, - open_end_points, - label="QET Wire Duct Open End {0} {1}".format(label, end_index), - project_uuid=project_uuid, - kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, - capacity=capacity, - ) + open_end_carrier = create_route_carrier( + doc, + open_end_points, + label="QET Wire Duct Open End {0} {1}".format(label, end_index), + project_uuid=project_uuid, + kind=ROUTE_CARRIER_KIND_WIRE_DUCT_OPEN_END, + capacity=capacity, ) + source_created.append(open_end_carrier) + created.append(open_end_carrier) + _mark_wire_duct_source(source, carrier, source_created) return created diff --git a/tests/python/freecad_exchange_auto_routing_test.py b/tests/python/freecad_exchange_auto_routing_test.py index 74fb726..327b2b4 100644 --- a/tests/python/freecad_exchange_auto_routing_test.py +++ b/tests/python/freecad_exchange_auto_routing_test.py @@ -947,6 +947,38 @@ class AutoRoutingTest(unittest.TestCase): len([item for item in carriers if item.QetRouteCarrierKind == "WireDuctOpenEnd"]), ) + def test_generate_routing_paths_refreshes_selected_wire_duct_geometry(self): + _install_fake_freecad() + terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() + auto_routing_panel = importlib.import_module("AutoRoutingPanel") + app = sys.modules["FreeCAD"] + gui = sys.modules["FreeCADGui"] + doc = FakeDocument() + app.ActiveDocument = doc + terminal_objects.ensure_root_group(doc, "project-1") + duct = doc.addObject("Part::Feature", "UnlabeledLongDuct") + duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) + gui.Selection = types.SimpleNamespace( + getSelection=lambda: [], + getSelectionEx=lambda: [FakeSelectionItem(obj=duct)], + ) + + auto_routing_panel.AutoRoutingController().generate_routing_paths() + duct.Shape = FakeShape(FakeBoundBox(0, 220, -10, 10, 0, 20)) + second = auto_routing_panel.AutoRoutingController().generate_routing_paths() + carriers = routing_network.collect_route_carriers(doc) + main = [item for item in carriers if item.QetRouteCarrierKind == "WireDuct"][0] + open_end_x_values = sorted( + point.x + for item in carriers + if item.QetRouteCarrierKind == "WireDuctOpenEnd" + for point in item.Points + ) + + self.assertEqual(0, second["selected_wire_duct_carriers"]) + self.assertEqual([(20.0, 0.0, 10.0), (200.0, 0.0, 10.0)], [(p.x, p.y, p.z) for p in main.Points]) + self.assertEqual([20.0, 20.0, 200.0, 200.0], open_end_x_values) + def test_prepare_layout_space_uses_whole_document_not_selected_face_workflow(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules()