import importlib import json import sys import types import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange" if str(MODULE_DIR) not in sys.path: sys.path.insert(0, str(MODULE_DIR)) def _install_fake_freecad(): class Vector: def __init__(self, x=0.0, y=0.0, z=0.0): self.x = float(x) self.y = float(y) self.z = float(z) class Rotation: def multVec(self, vector): return vector class Placement: def __init__(self, base=None, rotation=None): self.Base = base or Vector() self.Rotation = rotation or Rotation() def multVec(self, vector): return Vector( self.Base.x + vector.x, self.Base.y + vector.y, self.Base.z + vector.z, ) fake_freecad = types.ModuleType("FreeCAD") fake_freecad.Vector = Vector fake_freecad.Rotation = Rotation fake_freecad.Placement = Placement fake_freecad.ActiveDocument = None fake_freecad.Console = types.SimpleNamespace( PrintMessage=lambda *args, **kwargs: None, PrintWarning=lambda *args, **kwargs: None, PrintError=lambda *args, **kwargs: None, ) sys.modules["FreeCAD"] = fake_freecad fake_gui = types.ModuleType("FreeCADGui") fake_gui.addCommand = lambda *args, **kwargs: None fake_gui.SendMsgToActiveView = lambda *args, **kwargs: None fake_gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [], ) sys.modules["FreeCADGui"] = fake_gui fake_part = types.ModuleType("Part") fake_part.makePolygon = lambda points: tuple(points) sys.modules["Part"] = fake_part class FakeViewObject: def __init__(self): self.Visibility = True self.LineWidth = None self.LineColor = None self.DrawStyle = None class FakeObject: def __init__(self, name, type_id, doc=None): self.Name = name self.Label = name self.TypeId = type_id self.Document = doc self.PropertiesList = [] self.Group = [] self.ViewObject = FakeViewObject() self.Shape = None self.Points = [] self.Placement = sys.modules["FreeCAD"].Placement() self.InList = [] def isDerivedFrom(self, type_name): if self.TypeId == type_name: return True if type_name == "App::DocumentObjectGroup": return self.TypeId == "App::DocumentObjectGroup" if type_name == "App::LocalCoordinateSystem": return self.TypeId in {"Part::LocalCoordinateSystem", "PartDesign::CoordinateSystem"} return False def addProperty(self, prop_type, prop_name, group_name, description): if prop_name not in self.PropertiesList: self.PropertiesList.append(prop_name) def addObject(self, child): if child not in self.Group: self.Group.append(child) if self not in child.InList: child.InList.append(self) class FakeDocument: def __init__(self): self.Objects = [] self.Name = "FakeDoc" def addObject(self, type_name, name): obj = FakeObject(name, type_name, doc=self) self.Objects.append(obj) return obj def getObject(self, name): for obj in self.Objects: if obj.Name == name: return obj return None def removeObject(self, name): self.Objects = [obj for obj in self.Objects if obj.Name != name] def recompute(self): return None class FakeBoundBox: def __init__(self, xmin, xmax, ymin, ymax, zmin, zmax): self.XMin = xmin self.XMax = xmax self.YMin = ymin self.YMax = ymax self.ZMin = zmin self.ZMax = zmax class FakeShape: def __init__(self, bbox, edges=None, faces=None): self.BoundBox = bbox self.Edges = edges or [] self.Faces = faces or [] self.Solids = [] self.Shells = [] class FakeVertex: def __init__(self, point): self.Point = point class FakeEdge: ShapeType = "Edge" def __init__(self, start, end): self.Vertexes = [FakeVertex(start), FakeVertex(end)] class FakeFace: ShapeType = "Face" def __init__(self, bbox, normal, vertices=None, center=None): self.BoundBox = bbox self._normal = normal self.Vertexes = [FakeVertex(point) for point in (vertices or [])] self.CenterOfMass = center def normalAt(self, _u, _v): return self._normal class FakeSelectionItem: def __init__(self, sub_objects=None, obj=None): self.SubObjects = sub_objects or [] self.PickedPoints = [] self.Object = obj def _reload_modules(): for name in [ "TerminalObjects", "TemplateSemantics", "WiringObjects", "RoutingNetwork", "AutoRouting", "AutoRoutingPanel", ]: sys.modules.pop(name, None) terminal_objects = importlib.import_module("TerminalObjects") wiring_objects = importlib.import_module("WiringObjects") routing_network = importlib.import_module("RoutingNetwork") auto_routing = importlib.import_module("AutoRouting") return terminal_objects, wiring_objects, routing_network, auto_routing def _terminal(doc, terminal_objects, name, terminal_uuid, point): app = sys.modules["FreeCAD"] terminal = doc.addObject("Part::LocalCoordinateSystem", name) terminal.Placement = app.Placement(point, app.Rotation()) terminal_objects.set_terminal_semantics( terminal, "project-1", "element-{0}".format(terminal_uuid), terminal_uuid, "instance-{0}".format(terminal_uuid), label=terminal_uuid, ) return terminal class AutoRoutingTest(unittest.TestCase): def test_eplan_connection_route_selected_terminals_requires_supported_route_by_default(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, 20, 0)) with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals(doc, start, end) def test_eplan_connection_route_prefers_user_route_carrier_network(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(120, 0, 0)) routing_network.create_route_carrier( doc, [ app.Vector(0, 0, 20), app.Vector(0, 30, 20), app.Vector(120, 30, 20), app.Vector(120, 0, 20), ], project_uuid="project-1", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) self.assertTrue(any(point.y == 30.0 for point in result["points"])) def test_eplan_connection_route_stores_length_and_wire_style_diagnostics(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", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", wire_label="N4111", options={"wire_style_id": "42"}, ) wire = result["wire"] payload = json.loads(wire.QetRouteDiagnosticsJson) self.assertGreater(float(wire.QetRouteLengthMm), 0.0) self.assertEqual("42", wire.QetWireStyleId) self.assertEqual("42", payload["wire_style_id"]) self.assertGreater(payload["length_mm"], 0.0) self.assertTrue(payload["route_track"]["segments"]) self.assertEqual("WireDuct", payload["route_track"]["segments"][0]["carrier"]["kind"]) self.assertTrue(json.loads(wire.QetRouteTrackJson)["carrier_names"]) def test_route_track_preserves_generated_carrier_source_metadata(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"] doc = FakeDocument() app.ActiveDocument = doc 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)) duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "线槽A" duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) auto_routing_panel.AutoRoutingController().generate_routing_paths() result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) route_track = json.loads(result["wire"].QetRouteTrackJson) wire_duct_carriers = [ segment["carrier"] for segment in route_track["segments"] if segment["carrier"]["kind"] == "WireDuct" ] self.assertTrue(wire_duct_carriers) self.assertEqual("WireDuctA", wire_duct_carriers[0].get("source_name")) self.assertEqual("线槽A", wire_duct_carriers[0].get("source_label")) self.assertEqual("WireDuct", wire_duct_carriers[0].get("source_kind")) def test_route_track_records_carrier_capacity(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", kind="WireDuct", capacity=3, ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) route_track = json.loads(result["wire"].QetRouteTrackJson) self.assertEqual(3, route_track["segments"][0]["carrier"]["capacity"]) def test_network_eplan_connection_route_offsets_lane_by_route_index(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", kind="WireDuct", ) first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, route_index=0, wire_uuid="wire-1", options={"lane_spacing": 12.0, "lane_axis": "y"}, ) second = auto_routing.route_eplan_connection_between_terminals( doc, start, end, route_index=1, wire_uuid="wire-2", options={"lane_spacing": 12.0, "lane_axis": "y"}, ) third = auto_routing.route_eplan_connection_between_terminals( doc, start, end, route_index=2, wire_uuid="wire-3", options={"lane_spacing": 12.0, "lane_axis": "y"}, ) payload = json.loads(second["wire"].QetRouteDiagnosticsJson) third_payload = json.loads(third["wire"].QetRouteDiagnosticsJson) self.assertTrue(any(abs(point.y - 0.0) <= 0.001 for point in first["points"][1:-1])) self.assertTrue(any(abs(point.y - 12.0) <= 0.001 for point in second["points"][1:-1])) self.assertTrue(any(abs(point.y + 12.0) <= 0.001 for point in third["points"][1:-1])) self.assertEqual(1, payload["lane"]["index"]) self.assertEqual("y", payload["lane"]["axis"]) self.assertEqual(12.0, payload["lane"]["offset_mm"]) self.assertEqual(2, third_payload["lane"]["index"]) self.assertEqual(-12.0, third_payload["lane"]["offset_mm"]) def test_network_eplan_connection_route_removes_collinear_network_points(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(50, 0, 20), app.Vector(100, 0, 20), ], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) point_tuples = [(point.x, point.y, point.z) for point in result["points"]] self.assertNotIn((50.0, 0.0, 20.0), point_tuples) self.assertEqual( [ (0.0, 0.0, 0.0), (0.0, 0.0, 20.0), (100.0, 0.0, 20.0), (100.0, 0.0, 0.0), ], point_tuples, ) def test_eplan_connection_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_eplan_connection_between_terminals(doc, start_old, end_old, wire_uuid="wire-1") auto_routing.route_eplan_connection_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_eplan_connection_route_keeps_existing_wire_when_replacement_fails(self): _install_fake_freecad() terminal_objects, wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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", kind="WireDuct", ) first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", )["wire"] routing_network.clear_route_carriers(doc) with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", ) routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual([first], routed_wires) self.assertIsNotNone(doc.getObject(first.Name)) def test_eplan_connection_route_keeps_existing_wire_when_new_geometry_creation_fails(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", kind="WireDuct", ) first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", )["wire"] original_create_wire_geometry = auto_routing._create_wire_geometry def failing_create_wire_geometry(_doc, _name, _points): raise RuntimeError("create failed") auto_routing._create_wire_geometry = failing_create_wire_geometry try: with self.assertRaises(RuntimeError): auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", ) finally: auto_routing._create_wire_geometry = original_create_wire_geometry routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual([first], routed_wires) self.assertIsNotNone(doc.getObject(first.Name)) def test_eplan_connection_route_cleans_up_half_created_wire_when_draft_fallback_fails(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", kind="WireDuct", ) first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", )["wire"] part_module = sys.modules["Part"] draft_module = sys.modules.get("Draft") if draft_module is None: draft_module = types.ModuleType("Draft") sys.modules["Draft"] = draft_module original_make_polygon = part_module.makePolygon original_make_wire = getattr(draft_module, "make_wire", None) original_add_object = doc.addObject def failing_make_polygon(*args, **kwargs): raise RuntimeError("part unavailable") def half_created_make_wire(points, closed=False, placement=None, face=None, support=None, bs2wire=False): obj = doc.addObject("Part::FeaturePython", "Wire") obj.Points = list(points) raise RuntimeError("draft failed") def failing_add_object(type_name, name): if type_name == "App::FeaturePython": raise RuntimeError("fallback failed") return original_add_object(type_name, name) part_module.makePolygon = failing_make_polygon draft_module.make_wire = half_created_make_wire doc.addObject = failing_add_object try: with self.assertRaises(RuntimeError): auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", ) finally: part_module.makePolygon = original_make_polygon if original_make_wire is None: delattr(draft_module, "make_wire") else: draft_module.make_wire = original_make_wire doc.addObject = original_add_object routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual([first], routed_wires) self.assertEqual(0, len([obj for obj in doc.Objects if obj.Name == "Wire"])) def test_eplan_connection_route_keeps_existing_wire_when_old_replacement_removal_fails(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", kind="WireDuct", ) first = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", )["wire"] original_remove = auto_routing._remove_routing_connection_objects def failing_remove(target_doc, objects): if first in list(objects or []): return 0 return original_remove(target_doc, objects) auto_routing._remove_routing_connection_objects = failing_remove try: with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", ) finally: auto_routing._remove_routing_connection_objects = original_remove routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual([first], routed_wires) def test_route_carrier_styles_make_generated_objects_distinguishable(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") wire_duct = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_range = routing_network.create_route_carrier( doc, [app.Vector(0, 10, 0), app.Vector(100, 10, 0)], project_uuid="project-1", kind="RoutingRange", ) terminal_access = routing_network.create_route_carrier( doc, [app.Vector(0, 20, 0), app.Vector(100, 20, 0)], project_uuid="project-1", kind="TerminalAccess", ) self.assertEqual((1.0, 0.55, 0.0), wire_duct.ViewObject.LineColor) self.assertEqual(4.0, wire_duct.ViewObject.LineWidth) self.assertEqual((0.0, 0.65, 0.35), routing_range.ViewObject.LineColor) self.assertEqual("Solid", routing_range.ViewObject.DrawStyle) self.assertEqual((0.65, 0.2, 1.0), terminal_access.ViewObject.LineColor) self.assertEqual("Solid", terminal_access.ViewObject.DrawStyle) def test_set_route_carriers_visibility_toggles_only_route_helpers(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") carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) device = doc.addObject("Part::Feature", "DeviceA") device.ViewObject.Visibility = True hidden = routing_network.set_route_carriers_visibility(doc, False) self.assertFalse(carrier.ViewObject.Visibility) shown = routing_network.set_route_carriers_visibility(doc, True) self.assertEqual(1, hidden) self.assertEqual(1, shown) self.assertTrue(carrier.ViewObject.Visibility) self.assertTrue(device.ViewObject.Visibility) def test_collect_route_carriers_ignores_deleted_object_references(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") carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) class DeletedObjectReference: Name = "DeletedCarrier" def __getattr__(self, name): if name == "QetRoutingRole": raise RuntimeError("Cannot access attribute 'QetRoutingRole' of deleted object") raise AttributeError(name) doc.Objects.append(DeletedObjectReference()) carriers = routing_network.collect_route_carriers(doc) self.assertEqual([carrier], carriers) def test_route_carrier_exposes_capacity_property_for_auto_routing(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") carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) self.assertIn("QetRouteCarrierCapacity", carrier.PropertiesList) self.assertEqual(1, carrier.QetRouteCarrierCapacity) def test_route_graph_connects_crossing_carriers_at_intersection(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(50, 50, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(50, -50, 0), app.Vector(50, 50, 0)], project_uuid="project-1", kind="WireDuct", ) network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual(5, len(network["nodes"])) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertIn((50.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) def test_route_graph_connects_overlapping_collinear_carriers(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(120, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(80, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(40, 0, 0), app.Vector(120, 0, 0)], project_uuid="project-1", kind="WireDuct", ) network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertIn((40.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) self.assertIn((80.0, 0.0, 0.0), [(point.x, point.y, point.z) for point in result["points"]]) self.assertGreaterEqual(network["segment_count"], 3) def test_route_graph_bridges_adjoining_wire_duct_gap_with_eplan_tolerance(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(50, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) network = routing_network.build_route_graph(doc) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual(1, network["bridged_segment_count"]) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) def test_route_graph_bridges_adjoining_user_path_to_wire_duct_gap(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(50, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="UserPath", ) network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) start_key, _start_distance = routing_network.nearest_node(network, app.Vector(0, 0, 20)) end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) result = routing_network.shortest_path_with_carriers(network, start_key, end_key) self.assertEqual(1, network["bridged_segment_count"]) self.assertIsNotNone(result) self.assertIn("UserPath", result["carrier_kinds"]) def test_route_graph_bridges_endpoint_to_nearby_segment_projection(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], project_uuid="project-1", kind="UserPath", ) network = routing_network.build_route_graph(doc, adjoining_duct_tolerance=15.0) start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) result = routing_network.shortest_path_with_carriers(network, start_key, end_key) self.assertEqual(1, network["bridged_segment_count"]) self.assertIsNotNone(result) self.assertIn((50000, 0, 20000), result["path"]) def test_auto_routing_uses_endpoint_to_segment_projection_bridge(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, "TerminalBranch", "terminal-branch", app.Vector(50, 50, 0)) end = _terminal(doc, terminal_objects, "TerminalMain", "terminal-main", 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", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], project_uuid="project-1", kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"adjoining_duct_tolerance": 15.0}, ) self.assertEqual("Routed", result["route_status"]) self.assertEqual(1, result["network"]["bridged_segments"]) self.assertEqual(1, result["route_track"]["bridged_segments"]) self.assertTrue(any(segment.get("is_bridge") for segment in result["route_track"]["segments"])) self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) def test_auto_routing_does_not_use_terminal_access_to_bridge_main_path_gap(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(40, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(40, 0, 20), app.Vector(60, 0, 20)], project_uuid="project-1", kind="TerminalAccess", ) routing_network.create_route_carrier( doc, [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_access_max_distance": 5.0}, ) def test_route_graph_projection_bridge_respects_blocked_bbox(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(50, 8, 20), app.Vector(50, 50, 20)], project_uuid="project-1", kind="UserPath", ) blocked_bboxes = [ { "xmin": 45.0, "xmax": 55.0, "ymin": 2.0, "ymax": 6.0, "zmin": 15.0, "zmax": 25.0, } ] network = routing_network.build_route_graph( doc, blocked_bboxes=blocked_bboxes, adjoining_duct_tolerance=15.0, ) start_key, _start_distance = routing_network.nearest_node(network, app.Vector(50, 50, 20)) end_key, _end_distance = routing_network.nearest_node(network, app.Vector(100, 0, 20)) result = routing_network.shortest_path_with_carriers(network, start_key, end_key) self.assertEqual(0, network["bridged_segment_count"]) self.assertGreaterEqual(network["blocked_segment_count"], 1) self.assertIsNone(result) def test_auto_routing_respects_adjoining_duct_tolerance_option(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(44, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"adjoining_duct_tolerance": 15.0}, ) self.assertEqual("Routed", result["route_status"]) self.assertEqual(1, result["network"]["bridged_segments"]) def test_auto_routing_uses_bridged_user_path_to_wire_duct_gap(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(50, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(60, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"adjoining_duct_tolerance": 15.0}, ) self.assertEqual("Routed", result["route_status"]) self.assertEqual(1, result["network"]["bridged_segments"]) self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) def test_connect_point_to_network_replaces_bridged_edge_without_stale_reverse_edge(self): _install_fake_freecad() _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(50, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(54, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) network = routing_network.build_route_graph(doc) original_keys = set(network["nodes"].keys()) bridge_keys = { key for key, point in network["nodes"].items() if point.x in {50.0, 54.0} } projected_key, _distance, mode = routing_network.connect_point_to_network(network, app.Vector(52, 0, 20)) new_keys = set(network["nodes"].keys()) - original_keys stale_bridge_edges = [ (left_key, right_key) for left_key, neighbors in network["edges"].items() for right_key, _weight, _carrier in neighbors if left_key in bridge_keys and right_key in bridge_keys ] self.assertEqual("segment_projection", mode) self.assertEqual(projected_key, next(iter(new_keys))) self.assertEqual([], stale_bridge_edges) self.assertEqual(4, network["segment_count"]) def test_eplan_connection_route_prefers_wire_duct_over_auxiliary_range(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(120, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], project_uuid="project-1", kind="RoutingRange", ) routing_network.create_route_carrier( doc, [ app.Vector(0, 0, 20), app.Vector(0, 40, 20), app.Vector(120, 40, 20), app.Vector(120, 0, 20), ], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertTrue(any(point.y == 40.0 for point in result["points"])) def test_surface_carrier_grid_supports_backplate_routing(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(120, 0, 0)) face = FakeFace( FakeBoundBox(0, 120, -20, 120, -1, -1), app.Vector(0, 0, 1), ) created = routing_network.create_surface_carriers_from_selection( doc, [FakeSelectionItem([face])], project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertGreater(len(created), 0) self.assertEqual("RoutingRange", getattr(created[0], "QetRouteCarrierKind", "")) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertTrue(any(point.z == 4.0 for point in result["points"])) def test_auto_detect_support_surface_creates_routing_range(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) cabinet = doc.addObject("Part::Feature", "Cabinet") cabinet.Label = "3D机柜" cabinet.Shape = FakeShape(FakeBoundBox(0, 300, 0, 80, 0, 400)) duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) self.assertEqual(6, len(created)) self.assertEqual(0, len(created_again)) self.assertTrue(all(carrier.QetRouteCarrierKind == "RoutingRange" for carrier in created)) self.assertEqual("RoutingRange", panel.QetRoutingSourceKind) self.assertEqual("SupportSurface", panel.QetRoutingObstacleMode) self.assertFalse(hasattr(cabinet, "QetRoutingSourceKind")) self.assertFalse(hasattr(duct, "QetRoutingSourceKind")) def test_auto_detect_support_surface_refreshes_routing_range_geometry(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) panel.Shape = FakeShape(FakeBoundBox(20, 140, 0, 5, 0, 100)) created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) carriers = routing_network.collect_route_carriers(doc) x_values = [ point.x for carrier in carriers if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" for point in carrier.Points ] self.assertEqual(6, len(created)) self.assertEqual(0, len(created_again)) self.assertEqual(6, len([carrier for carrier in carriers if carrier.QetRouteCarrierKind == "RoutingRange"])) self.assertEqual(20.0, min(x_values)) self.assertEqual(140.0, max(x_values)) def test_auto_detect_support_surface_adds_missing_routing_range_lanes_after_resize(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) panel.Shape = FakeShape(FakeBoundBox(0, 180, 0, 5, 0, 120)) created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" ] x_values = [point.x for carrier in carriers for point in carrier.Points] z_values = [point.z for carrier in carriers for point in carrier.Points] self.assertEqual(6, len(created)) self.assertEqual(1, len(created_again)) self.assertEqual(7, len(carriers)) self.assertEqual(180.0, max(x_values)) self.assertEqual(120.0, max(z_values)) def test_auto_detect_support_surface_removes_stale_routing_range_lanes_after_resize(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) panel.Shape = FakeShape(FakeBoundBox(0, 60, 0, 5, 0, 60)) created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "RoutingRange" ] x_values = [point.x for carrier in carriers for point in carrier.Points] z_values = [point.z for carrier in carriers for point in carrier.Points] self.assertEqual(6, len(created)) self.assertEqual(0, len(created_again)) self.assertEqual(4, len(carriers)) self.assertEqual(60.0, max(x_values)) self.assertEqual(60.0, max(z_values)) def test_auto_detect_support_surface_removes_carriers_and_obstacle_mode_when_source_invalid(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 120, 0, 120)) created_again = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) self.assertEqual(6, len(created)) self.assertEqual(0, len(created_again)) self.assertEqual([], routing_network.collect_route_carriers(doc)) self.assertEqual("", getattr(panel, "QetRoutingObstacleMode", "")) self.assertEqual("", getattr(panel, "QetRouteCarrierNamesJson", "")) def test_eplan_connection_route_can_use_auto_detected_support_surface(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, 10, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(120, 10, 0)) panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "Mounting Plate A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertGreater(len(created), 0) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) self.assertTrue(any(point.y == 10.0 for point in result["points"])) def test_prepare_layout_space_auto_detects_support_surface_sources(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "Mounting Plate A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) result = auto_routing_panel.AutoRoutingController().generate_layout_space() self.assertGreater(result["support_surface_sources"], 0) self.assertEqual("document", result["source_mode"]) def test_generate_routing_paths_uses_selected_wire_duct_entity(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)], ) result = auto_routing_panel.AutoRoutingController().generate_routing_paths() self.assertEqual(1, result["wire_duct_carriers"]) self.assertEqual("selection", result["source_mode"]) def test_generate_routing_paths_uses_selected_route_path_as_user_path(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") route_path = doc.addObject("Part::Feature", "UserRouteSketch") route_path.Label = "用户主路径A" route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[ FakeEdge(app.Vector(0, 0, 20), app.Vector(0, 80, 20)), FakeEdge(app.Vector(0, 80, 20), app.Vector(100, 80, 20)), ], ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) result = auto_routing_panel.AutoRoutingController().generate_routing_paths() carriers = routing_network.collect_route_carriers(doc) user_paths = [item for item in carriers if item.QetRouteCarrierKind == "UserPath"] self.assertEqual(1, result["user_path_carriers"]) self.assertEqual(1, len(user_paths)) self.assertEqual("UserRouteSketch", user_paths[0].QetRouteSourceName) self.assertEqual("用户主路径A", user_paths[0].QetRouteSourceLabel) self.assertEqual("selection", result["source_mode"]) def test_controller_creates_selected_user_paths_without_full_network_generation(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") route_path = doc.addObject("Part::Feature", "UserRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() carriers = routing_network.collect_route_carriers(doc) self.assertEqual(1, result["user_path_carriers"]) self.assertEqual(1, result["network"]["kinds"]["UserPath"]) self.assertEqual(1, len(carriers)) self.assertEqual("UserPath", carriers[0].QetRouteCarrierKind) def test_selected_points_object_can_be_used_as_user_path(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") route_path = doc.addObject("Part::Feature", "PointRoute") route_path.Points = [ app.Vector(0, 0, 20), app.Vector(40, 0, 20), app.Vector(40, 30, 20), ] gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() carriers = routing_network.collect_route_carriers(doc) self.assertEqual(1, result["user_path_carriers"]) self.assertEqual( [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0), (40.0, 30.0, 20.0)], [(point.x, point.y, point.z) for point in carriers[0].Points], ) def test_selected_user_path_copies_source_capacity(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") route_path = doc.addObject("Part::Feature", "PointRoute") route_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] route_path.QetRouteCarrierCapacity = 5 gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() carrier = routing_network.collect_route_carriers(doc)[0] self.assertEqual(5, carrier.QetRouteCarrierCapacity) def test_controller_create_user_paths_reports_removed_stale_source_carriers(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") route_path = doc.addObject("Part::Feature", "UserRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() doc.removeObject("UserRouteSketch") gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().create_user_paths_from_selection() self.assertEqual(1, result["removed_stale_carriers"]) self.assertEqual(0, result["network"]["carriers"]) self.assertEqual([], routing_network.collect_route_carriers(doc)) def test_terminal_access_uses_terminal_local_route_points_before_main_network(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 = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [10, 0, 0], [10, 30, 0]]) routing_network.create_route_carrier( doc, [app.Vector(10, 80, 0), app.Vector(110, 80, 0)], project_uuid="project-1", kind="UserPath", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=100.0, ) self.assertEqual(1, len(created)) self.assertEqual( [(0.0, 0.0, 0.0), (10.0, 0.0, 0.0), (10.0, 30.0, 0.0)], [(p.x, p.y, p.z) for p in created[0].Points[:3]], ) def test_generate_routing_paths_refreshes_selected_user_path_without_duplicate(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") route_path = doc.addObject("Part::Feature", "UserRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=route_path)], ) first = auto_routing_panel.AutoRoutingController().generate_routing_paths() route_path.Shape = FakeShape( FakeBoundBox(0, 200, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(200, 0, 20))], ) second = auto_routing_panel.AutoRoutingController().generate_routing_paths() user_paths = [ item for item in routing_network.collect_route_carriers(doc) if item.QetRouteCarrierKind == "UserPath" ] self.assertEqual(1, first["user_path_carriers"]) self.assertEqual(0, second["user_path_carriers"]) self.assertEqual(1, len(user_paths)) self.assertEqual([(0.0, 0.0, 20.0), (200.0, 0.0, 20.0)], [(p.x, p.y, p.z) for p in user_paths[0].Points]) def test_eplan_connection_route_can_use_generated_user_path(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(200, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], project_uuid="project-1", kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertIn("UserPath", result["route_track"]["carrier_kinds"]) def test_generate_routing_paths_does_not_duplicate_selected_wire_duct_carriers(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)], ) first = auto_routing_panel.AutoRoutingController().generate_routing_paths() second = auto_routing_panel.AutoRoutingController().generate_routing_paths() carriers = routing_network.collect_route_carriers(doc) self.assertEqual(1, first["selected_wire_duct_carriers"]) self.assertEqual(0, second["selected_wire_duct_carriers"]) self.assertEqual( 1, len([item for item in carriers if item.QetRouteCarrierKind == "WireDuct"]), ) self.assertEqual( 2, 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_generate_routing_paths_removes_generated_wire_duct_carriers_after_source_deleted(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) auto_routing_panel.AutoRoutingController().generate_routing_paths() generated = [ item for item in routing_network.collect_route_carriers(doc) if getattr(item, "QetRouteSourceName", "") == "WireDuctA" ] doc.removeObject("WireDuctA") auto_routing_panel.AutoRoutingController().generate_routing_paths() self.assertEqual(3, len(generated)) self.assertEqual([], routing_network.collect_route_carriers(doc)) 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() 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") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "Mounting Plate A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=panel)], ) result = auto_routing_panel.AutoRoutingController().generate_layout_space() self.assertGreater(result["support_surface_sources"], 0) self.assertEqual("document", result["source_mode"]) def test_generate_routing_path_network_adds_terminal_access_to_route_network(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"] doc = FakeDocument() app.ActiveDocument = doc 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)) duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) result = auto_routing_panel.AutoRoutingController().generate_routing_paths() result_again = auto_routing_panel.AutoRoutingController().generate_routing_paths() access_carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" ] self.assertEqual(1, result["wire_duct_carriers"]) self.assertEqual(2, result["wire_duct_open_end_carriers"]) self.assertEqual(2, result["terminal_access_carriers"]) self.assertEqual(0, result_again["wire_duct_carriers"]) self.assertEqual(0, result_again["wire_duct_open_end_carriers"]) self.assertEqual(2, result_again["terminal_access_carriers"]) self.assertEqual(2, len(access_carriers)) self.assertGreater(result["network"]["segments"], 0) def test_generate_routing_path_network_connects_terminal_access_to_nearest_segment_point(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalMid", "terminal-mid", app.Vector(50, 30, 0)) duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) auto_routing_panel.AutoRoutingController().generate_routing_paths() access_carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" ] self.assertEqual(1, len(access_carriers)) end_point = access_carriers[0].Points[-1] self.assertEqual((50.0, 0.0, 20.0), (end_point.x, end_point.y, end_point.z)) def test_terminal_access_prefers_larger_connected_network_over_nearer_isolated_stub(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)) routing_network.create_route_carrier( doc, [app.Vector(0, 1, 20), app.Vector(5, 1, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [ app.Vector(0, 10, 20), app.Vector(40, 10, 20), app.Vector(80, 10, 20), app.Vector(120, 10, 20), ], project_uuid="project-1", kind="WireDuct", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", ) self.assertEqual(1, len(created)) end_point = created[0].Points[-1] self.assertEqual((0.0, 10.0, 20.0), (end_point.x, end_point.y, end_point.z)) def test_connection_entry_candidates_prefer_wire_duct_over_terminal_access(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(0, 10, 20)], project_uuid="project-1", kind="TerminalAccess", ) routing_network.create_route_carrier( doc, [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], project_uuid="project-1", kind="WireDuct", ) network = routing_network.build_route_graph(doc) ranked = routing_network.rank_connection_point_candidates( network, routing_network.connection_point_candidates(network, app.Vector(0, 0, 20), limit=0), ) first_kind = getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "") self.assertEqual("WireDuct", first_kind) def test_terminal_access_prefers_wire_duct_over_nearer_routing_range(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)) routing_network.create_route_carrier( doc, [app.Vector(0, 1, 20), app.Vector(120, 1, 20)], project_uuid="project-1", kind="RoutingRange", ) routing_network.create_route_carrier( doc, [app.Vector(0, 10, 20), app.Vector(120, 10, 20)], project_uuid="project-1", kind="WireDuct", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", ) self.assertEqual(1, len(created)) end_point = created[0].Points[-1] self.assertEqual((0.0, 10.0, 20.0), (end_point.x, end_point.y, end_point.z)) def test_eplan_connection_route_enters_network_at_segment_projection(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(50, 0, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(150, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(200, 0, 20)], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("segment_projection", result["network"]["entry_point_mode"]) self.assertEqual("segment_projection", result["network"]["exit_point_mode"]) self.assertNotIn(0.0, [point.x for point in result["points"][1:-1]]) self.assertNotIn(200.0, [point.x for point in result["points"][1:-1]]) self.assertLess(result["length_mm"], 150.0) def test_generate_routing_path_network_adds_wiring_cut_out_carrier(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cut_out = doc.addObject("Part::Feature", "WiringCutoutA") cut_out.Label = "Wiring Cut-Out A" cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) result = auto_routing_panel.AutoRoutingController().generate_routing_paths() cut_out_carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" ] self.assertEqual(1, result["wiring_cut_out_carriers"]) self.assertEqual(1, len(cut_out_carriers)) self.assertEqual("PassThrough", cut_out.QetRoutingObstacleMode) def test_generate_routing_path_network_refreshes_wiring_cut_out_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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cut_out = doc.addObject("Part::Feature", "WiringCutoutA") cut_out.Label = "Wiring Cut-Out A" cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) first = auto_routing_panel.AutoRoutingController().generate_routing_paths() cut_out.Shape = FakeShape(FakeBoundBox(65, 75, -2, 2, 15, 25)) second = auto_routing_panel.AutoRoutingController().generate_routing_paths() cut_out_carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" ] self.assertEqual(1, first["wiring_cut_out_carriers"]) self.assertEqual(0, second["wiring_cut_out_carriers"]) self.assertEqual(1, len(cut_out_carriers)) self.assertEqual([(70.0, -22.0, 20.0), (70.0, 22.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) def test_wiring_cut_out_source_bridge_extension_controls_generated_path_length(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cut_out = doc.addObject("Part::Feature", "WiringCutoutA") cut_out.Label = "过线孔A" cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) cut_out.QetWiringCutOutBridgeExtensionMm = 8.0 auto_routing_panel.AutoRoutingController().generate_routing_paths() cut_out_carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "WiringCutOut" ] self.assertEqual(1, len(cut_out_carriers)) self.assertIn("QetWiringCutOutBridgeExtensionMm", cut_out.PropertiesList) self.assertEqual(8.0, cut_out.QetWiringCutOutBridgeExtensionMm) self.assertEqual([(50.0, -10.0, 20.0), (50.0, 10.0, 20.0)], [(p.x, p.y, p.z) for p in cut_out_carriers[0].Points]) def test_wiring_cut_out_bridges_nearby_ducts_on_both_sides_of_panel(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, -20, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 20, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, -20, 20), app.Vector(50, -20, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(50, 20, 20), app.Vector(100, 20, 20)], project_uuid="project-1", kind="WireDuct", ) cut_out = doc.addObject("Part::Feature", "WiringCutoutA") cut_out.Label = "过线孔A" cut_out.Shape = FakeShape(FakeBoundBox(45, 55, -2, 2, 15, 25)) auto_routing_panel.AutoRoutingController().generate_routing_paths() result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertIn("WiringCutOut", result["route_track"]["carrier_kinds"]) self.assertEqual(0, result["collision_count"]) def test_check_routing_path_network_writes_diagnostic_for_unconnected_terminal(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalFar", "terminal-far", app.Vector(5000, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) self.assertFalse(result["ok"]) self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) self.assertEqual(1, len(payload["unconnected_terminals"])) self.assertEqual("terminal-far", payload["unconnected_terminals"][0]["terminal_uuid"]) self.assertEqual(1000.0, payload["unconnected_terminals"][0]["terminal_access_max_distance_mm"]) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertIn("端子未接入", message) self.assertIn("terminal-far", message) self.assertIn("4900.0 mm", message) self.assertIn("端子接入最大距离 1000.0 mm", message) self.assertIn("补一段线槽/辅助路径", message) def test_check_routing_path_network_warns_for_long_terminal_access(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(900, 0, 20), app.Vector(1000, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=1000.0, ) result = auto_routing.check_eplan_routing_path_network( doc, project_uuid="project-1", options={"terminal_access_max_distance": 1000.0}, ) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertFalse(result["ok"]) self.assertEqual(1, len(payload["long_terminal_accesses"])) self.assertEqual("terminal-long-access", payload["long_terminal_accesses"][0]["terminal_uuid"]) self.assertEqual(900.0, payload["long_terminal_accesses"][0]["terminal_access_length_mm"]) self.assertIn("端子接入过长", message) self.assertIn("TerminalLongAccess", message) self.assertIn("terminal-long-access", message) self.assertIn("900.0 mm", message) def test_check_routing_path_network_warns_for_invalid_terminal_local_route_points(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") terminal = _terminal(doc, terminal_objects, "TerminalInvalidLocalPath", "terminal-invalid-local-path", app.Vector(0, 0, 0)) terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") terminal.QetTerminalLocalRoutePointsJson = "{not-valid-json" routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=1000.0, ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertFalse(result["ok"]) self.assertEqual(1, len(payload["invalid_terminal_local_routes"])) self.assertEqual( "terminal-invalid-local-path", payload["invalid_terminal_local_routes"][0]["terminal_uuid"], ) self.assertEqual( "QetTerminalLocalRoutePointsJson", payload["invalid_terminal_local_routes"][0]["property_name"], ) self.assertIn("端子局部路径无效", message) self.assertIn("terminal-invalid-local-path", message) def test_check_routing_path_network_uses_terminal_local_route_end_for_connectivity(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") terminal = _terminal(doc, terminal_objects, "TerminalLocalEndOnDuct", "terminal-local-end-on-duct", app.Vector(0, 0, 0)) terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") terminal.QetTerminalLocalRoutePointsJson = json.dumps([[0, 0, 0], [1000, 0, 0]]) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=100.0, ) result = auto_routing.check_eplan_routing_path_network( doc, project_uuid="project-1", options={"terminal_access_max_distance": 100.0}, ) self.assertEqual([], created) self.assertEqual([], result["diagnostic"]["unconnected_terminals"]) self.assertNotIn( "unconnected_terminals", [issue.get("code") for issue in result["diagnostic"]["issues"]], ) def test_format_routing_path_network_report_tolerates_malformed_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() diagnostic = { "issues": [{"code": "external_issue", "count": 1}], "unconnected_terminals": ["bad-terminal-sample"], "possible_breaks": ["bad-break-sample"], "isolated_components": ["bad-component-sample"], } message = auto_routing.format_routing_path_network_report(diagnostic) self.assertIn("布线路径网络检查发现", message) self.assertIn("首个问题:external_issue", message) def test_format_routing_path_network_report_calls_out_wire_duct_break_point(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="线槽A", project_uuid="project-1", kind="WireDuct", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertIn("线槽端点疑似断开", message) self.assertIn("线槽A", message) self.assertIn("(0.0, 0.0, 20.0)", message) self.assertIn("补齐相邻线槽", message) def test_check_routing_path_network_warns_when_network_is_empty(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertFalse(result["ok"]) self.assertEqual("empty_routing_path_network", payload["issues"][0]["code"]) self.assertEqual(0, payload["summary"]["segments"]) self.assertIn("布线路径网络为空", message) def test_check_routing_path_network_warns_for_invalid_route_carrier_geometry(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") carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="坏用户路径", project_uuid="project-1", kind="UserPath", ) carrier.Points = [app.Vector(0, 0, 20)] result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertFalse(result["ok"]) self.assertEqual(1, len(payload["invalid_route_carriers"])) self.assertEqual("UserPath", payload["invalid_route_carriers"][0]["carrier"]["kind"]) self.assertEqual(1, payload["invalid_route_carriers"][0]["point_count"]) self.assertIn("路径对象几何无效", message) self.assertIn("坏用户路径", message) def test_check_routing_path_network_warns_when_only_routing_range_is_available(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="RoutingRange", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) message = auto_routing.format_routing_path_network_report(result["diagnostic"]) self.assertFalse(result["ok"]) self.assertEqual(1, payload["routing_range_only_network"]["routing_range_carriers"]) self.assertEqual( 0, payload["routing_range_only_network"]["primary_route_carriers"], ) self.assertIn("routing_range_only_network", [issue.get("code") for issue in payload["issues"]]) self.assertIn("仅使用布线面兜底", message) def test_format_routing_path_network_report_includes_bridged_segment_count(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() diagnostic = { "summary": { "carriers": 5, "segments": 6, "nodes": 5, "bridged_segments": 1, }, "issues": [], "ok": True, } message = auto_routing.format_routing_path_network_report(diagnostic) self.assertIn("桥接 1 段相邻/投影主路径", message) def test_check_routing_path_network_uses_adjoining_duct_tolerance_option(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") for index, points in enumerate( ( [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], [app.Vector(100, 0, 20), app.Vector(100, 100, 20)], [app.Vector(100, 100, 20), app.Vector(0, 100, 20)], [app.Vector(0, 100, 20), app.Vector(0, 0, 20)], ), start=1, ): routing_network.create_route_carrier( doc, points, label="线槽{0}".format(index), project_uuid="project-1", kind="WireDuct", ) result = auto_routing.check_eplan_routing_path_network( doc, project_uuid="project-1", options={"adjoining_duct_tolerance": 15.0}, ) self.assertTrue(result["ok"]) self.assertEqual(1, result["diagnostic"]["summary"]["bridged_segments"]) self.assertEqual([], result["diagnostic"]["possible_breaks"]) def test_generate_routing_path_network_skips_far_terminal_access_to_protect_view_bbox(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) duct = doc.addObject("Part::Feature", "WireDuctFar") duct.Label = "Wire Duct Far" duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) result = auto_routing_panel.AutoRoutingController().generate_routing_paths() self.assertEqual(1, result["wire_duct_carriers"]) self.assertEqual(2, result["wire_duct_open_end_carriers"]) self.assertEqual(0, result["terminal_access_carriers"]) def test_auto_routing_controller_exposes_terminal_access_max_distance(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) duct = doc.addObject("Part::Feature", "WireDuctFar") duct.Label = "Wire Duct Far" duct.Shape = FakeShape(FakeBoundBox(5000, 5100, -5, 5, 15, 25)) controller = auto_routing_panel.AutoRoutingController() controller.set_terminal_access_max_distance(6000.0) result = controller.generate_routing_paths() self.assertEqual(1, result["terminal_access_carriers"]) self.assertEqual(6000.0, controller.routing_options()["terminal_access_max_distance"]) def test_auto_routing_controller_exposes_terminal_exit_length(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(50, 0, 0)) duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) controller = auto_routing_panel.AutoRoutingController() controller.set_terminal_exit_length(40.0) controller.generate_routing_paths() access_carriers = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteCarrierKind", "") == "TerminalAccess" ] self.assertEqual(1, len(access_carriers)) self.assertEqual( (50.0, 0.0, 40.0), tuple(getattr(access_carriers[0].Points[0], axis) for axis in ("x", "y", "z")), ) self.assertEqual(40.0, controller.routing_options()["terminal_exit_length"]) def test_route_eplan_connections_prepares_layout_space_like_eplan_route(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"] doc = FakeDocument() app.ActiveDocument = doc 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)) duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "Wire Duct A" duct.Shape = FakeShape(FakeBoundBox(0, 100, -5, 5, 15, 25)) app._qet_exchange_payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing_panel.AutoRoutingController().route_eplan_connections() self.assertEqual(1, report["routed"]) self.assertEqual("eplan-route-v1", report["routing_method"]) self.assertTrue(report["routing_path_network_updated"]) self.assertEqual(1, report["prepared_layout"]["wire_duct_carriers"]) self.assertEqual(1, report["routing_path_network"]["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_routing_controller_passes_adjoining_duct_tolerance_to_batch_route(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"] doc = FakeDocument() app.ActiveDocument = doc 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(1000, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) app._qet_exchange_payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing_panel.AutoRoutingController( options={"adjoining_duct_tolerance": 15.0} ).route_eplan_connections() self.assertEqual(1, report["routed"]) self.assertEqual(1, report["routes"][0]["network"]["bridged_segments"]) def test_auto_routing_controller_summary_uses_adjoining_duct_tolerance(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(44, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(56, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) summary = auto_routing_panel.AutoRoutingController( options={"adjoining_duct_tolerance": 15.0} ).summary() self.assertIn("桥接:1", summary) def test_auto_routing_controller_exposes_lane_spacing(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", 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", kind="WireDuct", ) app._qet_exchange_payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } controller = auto_routing_panel.AutoRoutingController() controller.set_lane_spacing(14.0) report = controller.route_eplan_connections() self.assertEqual(14.0, controller.routing_options()["lane_spacing"]) self.assertEqual(14.0, report["routes"][1]["lane"]["spacing_mm"]) self.assertEqual(14.0, report["routes"][1]["lane"]["offset_mm"]) def test_auto_routing_controller_exposes_lane_axis(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"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], project_uuid="project-1", kind="WireDuct", ) app._qet_exchange_payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } controller = auto_routing_panel.AutoRoutingController() controller.set_lane_spacing(8.0) controller.set_lane_axis("z") report = controller.route_eplan_connections() self.assertEqual("z", controller.routing_options()["lane_axis"]) self.assertEqual("z", report["routes"][1]["lane"]["axis"]) self.assertEqual(8.0, report["routes"][1]["lane"]["offset_mm"]) def test_auto_routing_controller_exposes_lane_max_offset(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"] doc = FakeDocument() app.ActiveDocument = doc 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", kind="WireDuct", ) controller = auto_routing_panel.AutoRoutingController() controller.set_lane_spacing(10.0) controller.set_lane_axis("y") controller.set_lane_max_offset(18.0) result = _auto_routing.route_eplan_connection_between_terminals( doc, start, end, route_index=21, options=controller.routing_options(), ) self.assertEqual(18.0, controller.routing_options()["lane_max_offset"]) self.assertEqual(18.0, result["lane"]["max_offset_mm"]) self.assertEqual(18.0, result["lane"]["offset_mm"]) def test_auto_routing_panel_command_button_style_keeps_text_visible(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") class FakeButton: def __init__(self): self.text = "" self.tooltip = "" self.minimum_height = 0 self.stylesheet = "" def setText(self, text): self.text = text def setToolTip(self, tooltip): self.tooltip = tooltip def setMinimumHeight(self, height): self.minimum_height = height def setStyleSheet(self, stylesheet): self.stylesheet = stylesheet button = FakeButton() auto_routing_panel._style_command_button(button, "生成布线连接", "按导线任务布线") self.assertEqual("生成布线连接", button.text) self.assertEqual("按导线任务布线", button.tooltip) self.assertGreaterEqual(button.minimum_height, 28) self.assertIn("color", button.stylesheet) def test_eplan_connection_route_rejects_far_network_entry_to_avoid_huge_render_bbox(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(5000, 0, 20), app.Vector(5100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals(doc, start, end) def test_route_eplan_connection_between_terminals_fails_without_network(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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(120, 30, 0)) with self.assertRaises(auto_routing.AutoRoutingError): auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual(0, len(wiring_objects.iter_routed_wire_objects(doc))) def test_surface_carrier_grid_uses_actual_rotated_face_plane(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") normal = app.Vector(0, 1, 1) vertices = [ app.Vector(0, 0, 0), app.Vector(100, 0, 0), app.Vector(0, 50, -50), app.Vector(100, 50, -50), ] face = FakeFace( FakeBoundBox(0, 100, 0, 50, -50, 0), normal, vertices=vertices, center=app.Vector(50, 25, -25), ) created = routing_network.create_surface_carriers_from_selection( doc, [FakeSelectionItem([face])], project_uuid="project-1", spacing=50.0, offset=10.0, margin=0.0, ) self.assertGreater(len(created), 0) first_point = created[0].Points[0] for carrier in created: for point in carrier.Points: # The rotated face is y + z = 0; after a 10 mm normal offset, # all generated points must stay on one parallel plane. self.assertAlmostEqual(first_point.y + first_point.z, point.y + point.z, places=6) def test_route_path_creation_ignores_whole_solid_object_edges(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") solid = doc.addObject("Part::Feature", "CabinetSolid") solid.Shape = FakeShape( FakeBoundBox(0, 100, 0, 100, 0, 10), edges=[FakeEdge(app.Vector(0, 0, 0), app.Vector(100, 0, 0))], faces=[object()], ) created = routing_network.create_carriers_from_selection( doc, [FakeSelectionItem(obj=solid)], project_uuid="project-1", ) self.assertEqual([], created) def test_route_path_creation_projects_line_to_selected_face(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") face = FakeFace( FakeBoundBox(0, 100, 0, 100, 0, 0), app.Vector(0, 0, 1), ) draft_line = doc.addObject("Part::Feature", "DraftLine") draft_line.Shape = FakeShape( FakeBoundBox(10, 90, 10, 90, 25, 35), edges=[FakeEdge(app.Vector(10, 10, 25), app.Vector(90, 90, 35))], ) created = routing_network.create_carriers_from_selection( doc, [ FakeSelectionItem([face]), FakeSelectionItem(obj=draft_line), ], project_uuid="project-1", ) self.assertEqual(1, len(created)) self.assertEqual([2.0, 2.0], [point.z for point in created[0].Points]) def test_wire_duct_entity_generates_centerline_and_marks_source_pass_through(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") duct = doc.addObject("Part::Feature", "WireDuct") duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) created = routing_network.create_wire_duct_carriers_from_selection( doc, [FakeSelectionItem(obj=duct)], project_uuid="project-1", margin=20.0, ) self.assertEqual(3, len(created)) carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0] open_ends = [item for item in created if item.QetRouteCarrierKind == "WireDuctOpenEnd"] self.assertEqual("WireDuct", carrier.QetRouteCarrierKind) self.assertEqual(2, len(open_ends)) self.assertEqual("PassThrough", duct.QetRoutingObstacleMode) self.assertEqual([(20.0, 0.0, 15.0), (100.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points]) def test_wire_duct_source_end_margin_controls_generated_centerline_length(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "线槽A" duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) duct.QetWireDuctEndMarginMm = 5.0 created = routing_network.create_wire_duct_carriers_from_document( doc, project_uuid="project-1", ) carrier = [item for item in created if item.QetRouteCarrierKind == "WireDuct"][0] self.assertIn("QetWireDuctEndMarginMm", duct.PropertiesList) self.assertEqual(5.0, duct.QetWireDuctEndMarginMm) self.assertEqual([(5.0, 0.0, 15.0), (115.0, 0.0, 15.0)], [(p.x, p.y, p.z) for p in carrier.Points]) def test_wire_duct_source_capacity_is_copied_to_generated_carriers(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") duct = doc.addObject("Part::Feature", "WireDuct") duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) duct.QetRouteCarrierCapacity = 4 created = routing_network.create_wire_duct_carriers_from_selection( doc, [FakeSelectionItem(obj=duct)], project_uuid="project-1", margin=20.0, ) self.assertIn("QetRouteCarrierCapacity", duct.PropertiesList) self.assertTrue(all(item.QetRouteCarrierCapacity == 4 for item in created)) def test_auto_detect_wire_ducts_ignores_cabinet_models(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") duct = doc.addObject("Part::Feature", "WireDuctA") duct.Label = "线槽A" duct.Shape = FakeShape(FakeBoundBox(0, 120, -10, 10, 5, 25)) cabinet = doc.addObject("Part::Feature", "Cabinet") cabinet.Label = "3D机柜" cabinet.Shape = FakeShape(FakeBoundBox(0, 300, 0, 80, 0, 400)) created = routing_network.create_wire_duct_carriers_from_document( doc, project_uuid="project-1", ) created_again = routing_network.create_wire_duct_carriers_from_document( doc, project_uuid="project-1", ) self.assertEqual(3, len(created)) self.assertEqual(0, len(created_again)) self.assertEqual(1, len([item for item in created if item.QetRouteCarrierKind == "WireDuct"])) self.assertEqual(2, len([item for item in created if item.QetRouteCarrierKind == "WireDuctOpenEnd"])) self.assertEqual("PassThrough", duct.QetRoutingObstacleMode) self.assertFalse(hasattr(cabinet, "QetRoutingObstacleMode")) def test_wire_duct_source_is_not_reported_as_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(120, 0, 0)) duct = doc.addObject("Part::Feature", "WireDuct") duct.Shape = FakeShape(FakeBoundBox(-10, 130, -10, 10, 15, 25)) routing_network.create_wire_duct_carriers_from_selection( doc, [FakeSelectionItem(obj=duct)], project_uuid="project-1", margin=0.0, ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) def test_eplan_connection_route_uses_alternate_carrier_to_avoid_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)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [ app.Vector(0, 0, 20), app.Vector(0, 50, 20), app.Vector(100, 50, 20), app.Vector(100, 0, 20), ], project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "CabinetObstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 15, 25)) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) self.assertTrue(result["network"]["obstacle_aware"]) self.assertGreaterEqual(result["network"]["blocked_segments"], 1) self.assertIn(50.0, [point.y for point in result["points"]]) def test_eplan_connection_route_prefers_entry_candidate_without_access_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(20, 0, 0), app.Vector(100, 0, 0)], label="Near Duct", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 30, 0), app.Vector(100, 30, 0)], label="Clear Duct", project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "AccessObstacle") obstacle.Label = "Access Obstacle" obstacle.Shape = FakeShape(FakeBoundBox(10, 15, -5, 5, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] for segment in result["route_track"]["segments"] ] self.assertIn("Clear Duct", labels) self.assertNotIn("Near Duct", labels) self.assertEqual(0, result["collision_count"]) def test_eplan_connection_route_chooses_clear_orthogonal_access_order(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(30, 30, 0), app.Vector(100, 30, 0)], label="Only Duct", project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "AccessOrderObstacle") obstacle.Shape = FakeShape(FakeBoundBox(10, 20, -5, 5, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) point_tuples = [(point.x, point.y, point.z) for point in result["points"]] self.assertIn((0.0, 30.0, 0.0), point_tuples) self.assertNotIn((30.0, 0.0, 0.0), point_tuples) self.assertEqual(0, result["collision_count"]) def test_eplan_connection_route_marks_collision_warning_against_obstacle_bbox(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, 100), app.Vector(100, 0, 100)], project_uuid="project-1", ) obstacle = doc.addObject("Part::Feature", "Obstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("CollisionWarning", result["route_status"]) self.assertEqual("CollisionWarning", result["wire"].RouteStatus) self.assertEqual(1, result["collision_count"]) self.assertEqual("HardIntersection", result["collisions"][0]["collision_kind"]) def test_eplan_connection_route_marks_clearance_warning_against_expanded_obstacle_bbox(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, 100), app.Vector(100, 0, 100)], project_uuid="project-1", ) obstacle = doc.addObject("Part::Feature", "NearObstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 3, 6, 90, 110)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"obstacle_clearance": 5.0}, ) self.assertEqual("CollisionWarning", result["route_status"]) self.assertEqual(1, result["collision_count"]) self.assertEqual("ClearanceWarning", result["collisions"][0]["collision_kind"]) self.assertEqual(3.0, result["collisions"][0]["obstacle_bbox"]["ymin"]) self.assertEqual(-2.0, result["collisions"][0]["collision_bbox"]["ymin"]) def test_eplan_connection_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_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) def test_eplan_connection_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_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) def test_route_eplan_connections_from_payload_skips_missing_terminal(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)) payload = { "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", } ] } report = auto_routing.route_eplan_connections_from_payload(doc, payload) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["skipped_missing_terminal"]) self.assertEqual(1, report["available_terminals"]) self.assertEqual(0, report["local_terminals"]) self.assertEqual(["terminal-missing"], report["missing_endpoint_uuids"]) self.assertEqual("terminal-start", report["missing_endpoint_samples"][0]["start_terminal_uuid"]) self.assertTrue(report["missing_endpoint_samples"][0]["start_found"]) self.assertFalse(report["missing_endpoint_samples"][0]["end_found"]) def test_route_eplan_connections_from_payload_skips_resolved_tasks_without_route_network(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(1000, 0, 0)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-{0}".format(index), "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } for index in range(3) ], } original_route = auto_routing.route_eplan_connection_between_terminals def fail_if_called(*_args, **_kwargs): raise AssertionError("batch route must not call per-wire routing without route carriers") auto_routing.route_eplan_connection_between_terminals = fail_if_called try: report = auto_routing.route_eplan_connections_from_payload(doc, payload) finally: auto_routing.route_eplan_connection_between_terminals = original_route self.assertEqual(0, report["routed"]) self.assertEqual(3, report["skipped_missing_route_network"]) self.assertEqual(3, report["route_status_counts"]["MissingRouteNetwork"]) self.assertEqual([], report["errors"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) def test_route_eplan_connection_tasks_marks_task_missing_route_network_when_skipped(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)) task = wiring_objects.create_wire_task( doc, "project-1", "wire-missing-network", "N1", "terminal-start", "terminal-end", "instance-a", "instance-b", ) task.RouteStatus = "Routed" report = auto_routing.route_eplan_connection_tasks(doc) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["skipped_missing_route_network"]) self.assertEqual("MissingRouteNetwork", task.RouteStatus) def test_eplan_connection_route_prefers_wire_duct_over_shorter_routing_range(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(300, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(300, 0, 20)], project_uuid="project-1", kind="RoutingRange", ) routing_network.create_route_carrier( doc, [ app.Vector(0, 0, 20), app.Vector(0, 1200, 20), app.Vector(300, 1200, 20), app.Vector(300, 0, 20), ], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) def test_eplan_connection_route_prefers_wire_duct_when_routing_range_is_only_moderately_shorter(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(10, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], project_uuid="project-1", kind="RoutingRange", ) routing_network.create_route_carrier( doc, [ app.Vector(0, 0, 20), app.Vector(0, 145, 20), app.Vector(10, 145, 20), app.Vector(10, 0, 20), ], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) def test_eplan_connection_route_considers_primary_entry_beyond_nearest_surface_candidates(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)) for y in range(1, 11): routing_network.create_route_carrier( doc, [app.Vector(0, y, 20), app.Vector(100, y, 20)], project_uuid="project-1", kind="RoutingRange", ) routing_network.create_route_carrier( doc, [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], project_uuid="project-1", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertIn("WireDuct", result["route_track"]["carrier_kinds"]) self.assertNotIn("RoutingRange", result["route_track"]["carrier_kinds"]) def test_route_eplan_connections_from_payload_skips_tasks_when_carriers_have_no_segments(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)) broken_carrier = doc.addObject("Part::Feature", "BrokenCarrier") terminal_objects.ensure_string_property( broken_carrier, "QetRoutingRole", "QET Routing", "Routing role marker", "RoutingCarrier", ) terminal_objects.ensure_string_property( broken_carrier, "QetRouteCarrierKind", "QET Routing", "Route carrier kind", "WireDuct", ) terminal_objects.ensure_bool_property( broken_carrier, "CanRouteWire", "QET Routing", "Whether routing connections can use this path", True, ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"network_entry_max_distance": 30.0}, ) self.assertEqual(1, report["route_network_carriers"]) self.assertEqual(0, report["route_network_segments"]) self.assertEqual(0, report["route_network_nodes"]) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["skipped_missing_route_network"]) self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) self.assertEqual([], report["errors"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) def test_route_eplan_connections_classifies_disconnected_network_as_missing_route_network(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, 20), app.Vector(10, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 20), app.Vector(1010, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "start_element_uuid": "QF1", "start_terminal_display": "A1", "end_terminal_uuid": "terminal-end", "end_element_uuid": "KM1", "end_terminal_display": "13", }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"network_entry_max_distance": 30.0}, ) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["skipped_missing_route_network"]) self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) self.assertEqual([], report["errors"]) self.assertEqual("wire-a", report["missing_route_network_samples"][0]["wire_uuid"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) def test_network_entry_uses_terminal_access_max_distance_when_smaller(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(500, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(10, 0, 20)], project_uuid="project-1", kind="WireDuct", ) route = auto_routing.build_network_route( start, end, options={"terminal_access_max_distance": 30.0}, doc=doc, ) self.assertIsNone(route) def test_route_eplan_connections_writes_diagnostic_object_for_missing_terminal(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)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") self.assertEqual(1, report["skipped_missing_terminal"]) self.assertIsNotNone(diagnostic_group) self.assertEqual(1, len(diagnostic_group.Group)) diagnostic = diagnostic_group.Group[0] self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) def test_route_eplan_connections_writes_compact_batch_diagnostic(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 20, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 20, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 20, 20), app.Vector(100, 20, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "wire_label": "N1", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "wire_label": "N2", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) self.assertEqual(2, len(report["routes"])) self.assertNotIn("routes", diagnostic_payload) self.assertEqual(2, diagnostic_payload["route_sample_count"]) self.assertEqual(2, len(diagnostic_payload["route_samples"])) self.assertEqual("wire-a", diagnostic_payload["route_samples"][0]["wire_uuid"]) self.assertEqual("Routed", diagnostic_payload["route_samples"][0]["route_status"]) def test_compact_route_sample_prefers_route_track_bridged_segment_count(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-bridge", "route_track": { "bridged_segments": 1, }, "network": { "bridged_segments": 3, }, } ) self.assertEqual(1, sample["network"]["bridged_segments"]) def test_compact_route_sample_ignores_bridge_only_carrier_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-bridge", "route_track": { "carrier_kinds": {"RoutingRange": 1}, "carrier_names": ["VirtualBridge"], "segments": [ { "is_bridge": True, "carrier": {"name": "VirtualBridge", "kind": "RoutingRange"}, }, { "carrier": {"name": "WireDuctA", "kind": "WireDuct"}, }, ], }, } ) self.assertEqual({"WireDuct": 1}, sample["carrier_kinds"]) self.assertEqual(["WireDuctA"], sample["carrier_names"]) def test_route_eplan_connections_batch_diagnostic_includes_quality_warnings(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="RoutingRange", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-surface", "wire_label": "N-SURFACE", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") diagnostic_payload = json.loads(diagnostic_group.Group[0].QetDiagnosticJson) self.assertEqual(1, report["routed"]) self.assertEqual(1, diagnostic_payload["route_quality_warning_count"]) self.assertEqual( "wire-surface", diagnostic_payload["route_quality_warning_samples"][0]["wire_uuid"], ) self.assertEqual( ["RoutingRange"], diagnostic_payload["route_quality_warning_samples"][0]["carrier_kinds"], ) def test_compact_batch_report_includes_entry_distance_warning_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "terminal_access_warning_distance": 100.0, "routes": [ { "wire_uuid": "wire-long-entry", "wire_label": "N-LONG", "network": { "entry_distance": 125.0, "exit_distance": 20.0, }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["route_entry_distance_warning_count"]) self.assertEqual( "wire-long-entry", payload["route_entry_distance_warning_samples"][0]["wire_uuid"], ) self.assertEqual( ["entry"], payload["route_entry_distance_warning_samples"][0]["warning_sides"], ) def test_route_eplan_connections_reports_total_connection_route_length(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) self.assertGreater(report["total_length_mm"], 0.0) self.assertEqual(report["total_length_mm"], report["routes"][0]["length_mm"]) self.assertIn("总长度", message) def test_route_eplan_connections_hides_route_carriers_after_routing_by_default(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)) carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections( doc, payload=payload, update_network=False, ) self.assertEqual(1, report["routed"]) self.assertEqual(1, report["hidden_route_carriers"]) self.assertFalse(carrier.ViewObject.Visibility) def test_route_eplan_connections_batch_recomputes_once_after_created_wires(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 10, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 10, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], project_uuid="project-1", kind="WireDuct", ) recompute_count = {"value": 0} def count_recompute(): recompute_count["value"] += 1 doc.recompute = count_recompute payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) self.assertEqual(2, report["routed"]) self.assertEqual(1, recompute_count["value"]) def test_route_eplan_connections_replaces_existing_routed_wires_for_same_batch(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-repeat", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } first = auto_routing.route_eplan_connections_from_payload(doc, payload) second = auto_routing.route_eplan_connections_from_payload(doc, payload) routed_wires = list(wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual(1, first["routed"]) self.assertEqual(1, second["routed"]) self.assertEqual(1, second["replaced_routed_connections"]) self.assertEqual(1, len(routed_wires)) self.assertEqual("wire-repeat", routed_wires[0].QetWireUuid) def test_clear_routing_connections_resets_task_status_and_batch_diagnostics(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) task = wiring_objects.create_wire_task( doc, "project-1", "wire-clear", "N1", "terminal-start", "terminal-end", "instance-a", "instance-b", ) report = auto_routing.route_eplan_connection_tasks(doc) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") self.assertEqual(1, report["routed"]) self.assertEqual("Routed", task.RouteStatus) self.assertEqual(1, len(list(getattr(diagnostic_group, "Group", []) or []))) removed = auto_routing.clear_routing_connections(doc) self.assertEqual(1, removed) self.assertEqual("Task", task.RouteStatus) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual([], list(getattr(diagnostic_group, "Group", []) or [])) def test_route_report_includes_route_source_sample_when_available(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "wire_label": "N4111", "route_track": { "segments": [ {"carrier": {"kind": "TerminalAccess", "source_label": "QF1:A1"}}, {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, {"carrier": {"kind": "WiringCutOut", "source_label": "过线孔A"}}, {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径示例:导线 N4111 经过 QF1:A1、线槽A、过线孔A。", message) def test_route_report_source_sample_skips_bridge_segments(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "wire_label": "N4111", "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "线槽A"}}, {"is_bridge": True, "carrier": {"kind": "WireDuct", "source_label": "虚拟桥接"}}, {"carrier": {"kind": "UserPath", "source_label": "用户路径B"}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径示例:导线 N4111 经过 线槽A、用户路径B。", message) self.assertNotIn("虚拟桥接", message) def test_route_track_segment_keys_skip_bridge_segments(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() route_track = { "segments": [ { "from_key": [0, 0, 0], "to_key": [100, 0, 0], "carrier": {"name": "WireDuctA"}, }, { "is_bridge": True, "from_key": [100, 0, 0], "to_key": [100, 10, 0], "carrier": {"name": "VirtualBridge"}, }, ] } keys = auto_routing._route_track_segment_keys(route_track) self.assertEqual(1, len(keys)) self.assertEqual("WireDuctA", keys[0][0]) def test_route_quality_warning_ignores_bridge_only_routing_range(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "wire_label": "N4111", "route_track": { "carrier_kinds": {"RoutingRange": 1}, "segments": [ { "is_bridge": True, "carrier": {"kind": "RoutingRange", "source_label": "虚拟布线面桥接"}, } ], }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertNotIn("路径质量提示", message) def test_route_report_includes_network_bridge_and_blocked_segment_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "network": { "bridged_segments": 1, "blocked_segments": 2, }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径,避障屏蔽 2 段。", message) def test_route_report_prefers_route_track_bridged_segment_count(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "network": { "bridged_segments": 3, }, "route_track": { "bridged_segments": 1, }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径网络:自动桥接 1 段相邻/投影主路径。", message) self.assertNotIn("自动桥接 3 段", message) def test_route_report_includes_parallel_lane_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ {"lane": {"index": 0, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": 0.0}}, {"lane": {"index": 2, "axis": "y", "spacing_mm": 10.0, "max_offset_mm": 30.0, "offset_mm": -10.0}}, ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("并行错位:最大 lane 2,间距 10.0 mm,最大偏移 30.0 mm。", message) def test_eplan_connection_lane_offset_is_capped_for_dense_parallel_routes(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", kind="WireDuct", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, route_index=21, options={"lane_spacing": 10.0, "lane_axis": "y"}, ) self.assertEqual(30.0, result["lane"]["offset_mm"]) self.assertLessEqual( max(abs(point.y) for point in result["points"]), 30.0, ) def test_route_report_includes_replaced_routed_connection_count(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "replaced_routed_connections": 2, } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("已替换旧布线连接:2 条。", message) def test_route_report_includes_hidden_route_carrier_count(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "hidden_route_carriers": 3, } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("已隐藏走线路径辅助对象:3 条。", message) def test_route_report_warns_when_routes_use_surface_or_auxiliary_paths(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "wire_label": "N1", "route_track": { "carrier_kinds": { "TerminalAccess": 2, "WireDuct": 1, "RoutingRange": 2, }, }, }, { "wire_label": "N2", "route_track": { "carrier_kinds": { "TerminalAccess": 2, "WireDuct": 3, }, }, }, ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径质量提示:1 条导线使用布线面/辅助路径", message) self.assertIn("示例 N1 使用布线面。", message) def test_route_report_warns_when_parallel_lanes_exceed_track_capacity(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 3, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "capacity": 2}}, {"carrier": {"kind": "WireDuct", "capacity": 4}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("容量提示:最大并行线数 3,路径最小容量 2。", message) def test_route_report_ignores_bridge_segments_for_capacity_pressure(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 3, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ {"is_bridge": True, "carrier": {"kind": "UserPath", "capacity": 1}}, {"carrier": {"kind": "WireDuct", "capacity": 4}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertNotIn("容量提示", message) def test_route_report_includes_entry_candidate_rank_when_route_uses_fallback_entry(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "wire_label": "N1", "network": { "entry_candidate_rank": 3, "exit_candidate_rank": 1, "entry_candidate_score": 125.0, }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("接入候选", message) self.assertIn("起点第 3 个", message) def test_route_report_warns_when_network_entry_distance_is_long(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "terminal_access_warning_distance": 100.0, "routes": [ { "wire_label": "N1", "network": { "entry_distance": 125.0, "exit_distance": 20.0, }, }, { "wire_label": "N2", "network": { "entry_distance": 20.0, "exit_distance": 150.0, }, }, ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("接入距离提示:2 条导线", message) self.assertIn("示例导线 N1", message) self.assertIn("起点接入 125.0 mm", message) def test_route_report_capacity_pressure_is_checked_per_route(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "capacity": 4}}, ] }, }, { "lane": {"index": 0, "spacing_mm": 10.0}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "capacity": 1}}, ] }, }, ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertNotIn("容量提示", message) def test_route_eplan_connections_report_keeps_route_identity_and_diagnostics(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "wire_style_id": "42", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"lane_spacing": 12.0, "lane_axis": "y"}, ) route = report["routes"][0] self.assertEqual("wire-1", route["wire_uuid"]) self.assertEqual("N4111", route["wire_label"]) self.assertEqual("42", route["wire_style_id"]) self.assertEqual("terminal-start", route["start_terminal_uuid"]) self.assertEqual("terminal-end", route["end_terminal_uuid"]) self.assertEqual(0, route["lane"]["index"]) self.assertEqual("network-dijkstra-v1", route["algorithm"]) self.assertEqual(1, route["network"]["carriers"]) self.assertEqual("WireDuct", route["route_track"]["segments"][0]["carrier"]["kind"]) def test_route_eplan_connections_can_skip_nearer_isolated_entry_network(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, 1, 20), app.Vector(5, 1, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 10, 20), app.Vector(100, 10, 20)], project_uuid="project-1", kind="WireDuct", ) 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_eplan_connections_from_payload(doc, payload) self.assertEqual(1, report["routed"]) self.assertEqual(0, len(report["errors"])) route = report["routes"][0] self.assertEqual("network-dijkstra-v1", route["algorithm"]) self.assertGreater(route["network"]["entry_distance"], 1.0) self.assertGreater(route["network"]["entry_candidate_rank"], 1) def test_route_eplan_connections_report_includes_routing_path_network_diagnostic(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="RoutingRange", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-range-only", "wire_label": "N-RANGE", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections( doc, payload=payload, options={"hide_route_carriers_after_route": False}, project_uuid="project-1", ) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["routed"]) self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) self.assertIn( "routing_range_only_network", report["routing_path_network_diagnostic"]["issue_codes"], ) self.assertIn("路径网络检查提示", message) self.assertIn("仅使用布线面兜底", message) def test_route_eplan_connections_preserves_endpoint_metadata_on_routed_wire(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_element_uuid": "device-a", "start_terminal_uuid": "terminal-start", "start_terminal_display": "A1", "end_element_uuid": "device-b", "end_terminal_uuid": "terminal-end", "end_terminal_display": "B1", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) routed_group = doc.getObject("QETWiring_04_Routed") wire = routed_group.Group[0] diagnostics = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual("device-a", wire.QetStartElementUuid) self.assertEqual("A1", wire.QetStartTerminalDisplay) self.assertEqual("device-b", wire.QetEndElementUuid) self.assertEqual("B1", wire.QetEndTerminalDisplay) self.assertEqual("device-a", report["routes"][0]["start_element_uuid"]) self.assertEqual("B1", report["routes"][0]["end_terminal_display"]) self.assertEqual("A1", diagnostics["endpoint_metadata"]["start_terminal_display"]) def test_route_eplan_connection_tasks_preserve_task_endpoint_labels_on_routed_wire(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, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) task = wiring_objects.create_wire_task( doc, "project-1", "wire-1", "N4111", "terminal-start", "terminal-end", "instance-a", "instance-b", ) terminal_objects.ensure_string_property(task, "QetStartDeviceLabel", "QET Wiring", "", "QF1") terminal_objects.ensure_string_property(task, "QetEndDeviceLabel", "QET Wiring", "", "X1") terminal_objects.ensure_string_property(task, "QetEndpointLabel", "QET Wiring", "", "QF1:A1 -> X1:B1") report = auto_routing.route_eplan_connection_tasks(doc) routed_group = doc.getObject("QETWiring_04_Routed") wire = routed_group.Group[0] diagnostics = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual("QF1", wire.QetStartDeviceLabel) self.assertEqual("X1", wire.QetEndDeviceLabel) self.assertEqual("QF1:A1 -> X1:B1", wire.QetEndpointLabel) self.assertEqual("QF1:A1 -> X1:B1", report["routes"][0]["endpoint_label"]) self.assertEqual("QF1", diagnostics["endpoint_metadata"]["start_device_label"]) def test_route_eplan_connections_records_wire_identity_for_errors(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)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-bad", "wire_label": "N500", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-start", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) self.assertEqual(1, len(report["errors"])) self.assertIn("error_samples", report) self.assertEqual("wire-bad", report["error_samples"][0]["wire_uuid"]) self.assertEqual("N500", report["error_samples"][0]["wire_label"]) self.assertEqual("terminal-start", report["error_samples"][0]["start_terminal_uuid"]) self.assertEqual("terminal-start", report["error_samples"][0]["end_terminal_uuid"]) self.assertIn("different", report["error_samples"][0]["error"]) def test_route_eplan_connections_report_includes_readable_error_sample(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)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-bad", "wire_label": "N500", "start_element_uuid": "device-a", "start_terminal_uuid": "terminal-start", "start_terminal_display": "A1", "end_element_uuid": "device-a", "end_terminal_uuid": "terminal-start", "end_terminal_display": "A1", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual("device-a", report["error_samples"][0]["start_element_uuid"]) self.assertIn("错误示例:导线 N500", message) self.assertIn("device-a/A1 (terminal-start) -> device-a/A1 (terminal-start)", message) def test_route_eplan_connections_counts_route_statuses_for_summary(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, "RouteStart", "route-start", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "RouteEnd", "route-end", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "CollisionStart", "collision-start", app.Vector(0, 100, 0)) _terminal(doc, terminal_objects, "CollisionEnd", "collision-end", app.Vector(100, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 100, 100), app.Vector(100, 100, 100)], project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "CollisionObstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 90, 110, 90, 110)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-ok", "start_terminal_uuid": "route-start", "end_terminal_uuid": "route-end", }, { "wire_id": "wire-collision", "start_terminal_uuid": "collision-start", "end_terminal_uuid": "collision-end", }, { "wire_id": "wire-error", "start_terminal_uuid": "route-start", "end_terminal_uuid": "route-start", }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"avoid_obstacles": False}, ) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["route_status_counts"]["Routed"]) self.assertEqual(1, report["route_status_counts"]["CollisionWarning"]) self.assertEqual(1, report["route_status_counts"]["Error"]) self.assertIn("结果状态", message) self.assertIn("正常 1 条", message) self.assertIn("碰撞告警 1 条", message) self.assertIn("错误 1 条", message) def test_route_eplan_connections_lane_index_is_per_terminal_pair(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 100, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, { "wire_id": "wire-a-repeat", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"lane_spacing": 10.0, "lane_axis": "y"}, ) self.assertEqual(0, report["routes"][0]["lane"]["index"]) self.assertEqual(0, report["routes"][1]["lane"]["index"]) self.assertEqual(1, report["routes"][2]["lane"]["index"]) def test_route_eplan_connections_lane_index_increments_for_shared_route_segments(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", 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", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"lane_spacing": 10.0, "lane_axis": "y"}, ) self.assertEqual(0, report["routes"][0]["lane"]["index"]) self.assertEqual(1, report["routes"][1]["lane"]["index"]) routed_group = doc.getObject("QETWiring_04_Routed") self.assertEqual(2, len(list(getattr(routed_group, "Group", []) or []))) second_wire = [ wire for wire in list(getattr(routed_group, "Group", []) or []) if getattr(wire, "QetWireUuid", "") == "wire-b" ][0] self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) def test_route_eplan_connections_auto_lane_axis_offsets_perpendicular_to_shared_segment(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(0, 100, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(0, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(0, 100, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"lane_spacing": 10.0}, ) self.assertEqual(1, report["routes"][1]["lane"]["index"]) self.assertEqual("x", report["routes"][1]["lane"]["axis"]) routed_group = doc.getObject("QETWiring_04_Routed") second_wire = [ wire for wire in list(getattr(routed_group, "Group", []) or []) if getattr(wire, "QetWireUuid", "") == "wire-b" ][0] self.assertTrue(any(abs(point.x - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) self.assertFalse(all(abs(point.x) <= 0.001 for point in second_wire.Points[1:-1])) def test_route_eplan_connections_prefers_unused_alternate_route_segments(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="Direct Duct", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], label="Left Bridge", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="Alternate Duct", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], label="Right Bridge", project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) first_labels = [ segment["carrier"]["label"] for segment in report["routes"][0]["route_track"]["segments"] ] second_labels = [ segment["carrier"]["label"] for segment in report["routes"][1]["route_track"]["segments"] ] self.assertIn("Direct Duct", first_labels) self.assertIn("Alternate Duct", second_labels) self.assertNotIn("Direct Duct", second_labels) def test_route_eplan_connections_respects_route_segment_capacity_before_detouring(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, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartB", "terminal-start-b", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 0, 0)) _terminal(doc, terminal_objects, "TerminalStartC", "terminal-start-c", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEndC", "terminal-end-c", app.Vector(100, 0, 0)) direct = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="Direct Duct", project_uuid="project-1", kind="WireDuct", ) direct.QetRouteCarrierCapacity = 2 routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], label="Left Bridge", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="Alternate Duct", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], label="Right Bridge", project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", }, { "wire_id": "wire-c", "start_terminal_uuid": "terminal-start-c", "end_terminal_uuid": "terminal-end-c", }, ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) route_labels = [ [segment["carrier"]["label"] for segment in route["route_track"]["segments"]] for route in report["routes"] ] self.assertIn("Direct Duct", route_labels[0]) self.assertIn("Direct Duct", route_labels[1]) self.assertIn("Alternate Duct", route_labels[2]) self.assertNotIn("Direct Duct", route_labels[2]) def test_route_eplan_connections_prefers_unused_segments_occupied_by_existing_wires(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)], label="Direct Duct", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(0, 40, 20)], label="Left Bridge", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="Alternate Duct", project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(100, 40, 20), app.Vector(100, 0, 20)], label="Right Bridge", project_uuid="project-1", kind="WireDuct", ) auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="existing-wire", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "new-wire", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) route_labels = [ segment["carrier"]["label"] for segment in report["routes"][0]["route_track"]["segments"] ] self.assertIn("Alternate Duct", route_labels) self.assertNotIn("Direct Duct", route_labels) def test_route_eplan_connections_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_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_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("HardIntersection", report["collision_samples"][0]["collision_kind"]) self.assertEqual({"x": 0.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_start"]) self.assertEqual({"x": 100.0, "y": 0.0, "z": 100.0}, report["collision_samples"][0]["segment_end"]) self.assertEqual(40.0, report["collision_samples"][0]["obstacle_bbox"]["xmin"]) self.assertEqual(35.0, report["collision_samples"][0]["collision_bbox"]["xmin"]) self.assertEqual("Middle Obstacle", report["routes"][0]["collision_samples"][0]["obstacle_label"]) self.assertIn("碰撞示例", message) self.assertIn("Middle Obstacle", message) def test_route_eplan_connections_report_calls_out_local_unbound_terminals(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, "LocalTerminal", "local:instance-1:p1", app.Vector(0, 0, 0), ) payload = { "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "qet-terminal-start", "end_terminal_uuid": "qet-terminal-end", } ] } report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["available_terminals"]) self.assertEqual(1, report["local_terminals"]) self.assertIn("端子匹配失败", message) self.assertIn("local:", message) def test_route_eplan_connections_report_includes_network_and_first_error(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 1, "collision_warnings": 1, "skipped_missing_terminal": 1, "prepared_layout": { "wire_duct_carriers": 2, "surface_carriers": 4, "terminal_access_carriers": 6, }, "missing_endpoint_samples": [ { "start_terminal_uuid": "terminal-a", "end_terminal_uuid": "terminal-b", } ], "errors": ["没有可用的线槽/路由路径网络"], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("routed=1", message) self.assertIn("线槽路径 2 条", message) self.assertIn("首个错误:没有可用的线槽/路由路径网络", message) self.assertIn("缺失示例:terminal-a -> terminal-b", message) def test_route_eplan_connections_report_calls_out_missing_route_network(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 3, "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 0, "skipped_missing_route_network": 3, "route_status_counts": { "MissingRouteNetwork": 3, }, } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("缺少布线路径网络 3 条", message) self.assertIn("请先生成线槽、布线面或布线路径网络", message) def test_route_eplan_connections_report_includes_missing_route_network_sample(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 1, "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 0, "skipped_missing_route_network": 1, "missing_route_network_samples": [ { "wire_uuid": "wire-1", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "start_element_uuid": "QF1", "start_terminal_display": "A1", "end_terminal_uuid": "terminal-end", "end_element_uuid": "KM1", "end_terminal_display": "13", "error": "没有可用的布线路径网络:起点和终点无法连通", } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("缺路径网络示例:导线 N4111", message) self.assertIn("QF1/A1 (terminal-start) -> KM1/13 (terminal-end)", message) self.assertIn("原因:没有可用的布线路径网络:起点和终点无法连通", message) def test_route_eplan_connections_report_includes_readable_missing_endpoint_labels(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 1, "missing_endpoint_samples": [ { "start_terminal_uuid": "terminal-a", "start_element_uuid": "device-a", "start_terminal_display": "A1", "end_terminal_uuid": "terminal-b", "end_element_uuid": "device-b", "end_terminal_display": "B1", } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("缺失示例:device-a/A1 (terminal-a) -> device-b/B1 (terminal-b)", message) def test_route_eplan_connections_report_identifies_which_endpoint_is_missing(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 1, "missing_endpoint_samples": [ { "start_terminal_uuid": "terminal-start", "start_found": True, "end_terminal_uuid": "terminal-missing", "end_found": False, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("缺失示例:terminal-start -> terminal-missing", message) self.assertIn("缺失:终点", message) def test_route_eplan_connections_report_calls_out_clearance_collision_kind(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 1, "skipped_missing_terminal": 0, "collision_samples": [ { "wire_label": "N4111", "obstacle_label": "柜体侧板", "collision_kind": "ClearanceWarning", } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("安全间隙", message) self.assertIn("柜体侧板", message) def test_route_eplan_connections_report_ignores_non_numeric_status_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "route_status_counts": { "Routed": "1", "ExternalStatus": "not-a-number", }, } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("正常 1 条", message) self.assertNotIn("ExternalStatus", message) def test_bind_wire_task_terminals_from_payload_does_not_create_wires(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") root.addObject(device) terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-a") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-a") terminal_objects.ensure_string_property(device, "QetProjectUuid", "QET Exchange", "", "project-1") terminal_group = terminal_objects.ensure_terminal_group( doc, device, project_uuid="project-1", instance_id="instance-a", ) for slot_name, point in ( ("P1", app.Vector(0, 0, 0)), ("P2", app.Vector(100, 0, 0)), ): terminal = terminal_objects.create_lcs_object( doc, "QETTerminal_instance_a_{0}".format(slot_name), placement=app.Placement(point, app.Rotation()), label=slot_name, ) terminal_group.addObject(terminal) terminal_objects.set_terminal_semantics( terminal, "project-1", "device-a", "local:instance-a:{0}".format(slot_name), "instance-a", label=slot_name, slot_name=slot_name, ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_element_uuid": "device-a", "start_instance_id": "instance-a", "start_terminal_uuid": "qet-terminal-p1", "start_terminal_display": "P1", "end_element_uuid": "device-a", "end_instance_id": "instance-a", "end_terminal_uuid": "qet-terminal-p2", "end_terminal_display": "P2", } ], } report = auto_routing.bind_wire_task_terminals_from_payload(doc, payload) indexed = auto_routing.index_terminals(doc) self.assertEqual(2, report["bound"]) self.assertEqual(0, report["created"]) self.assertEqual(0, report["local_terminals"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) self.assertEqual("qet", indexed["qet-terminal-p1"].QetTerminalBindingMode) self.assertFalse(indexed["qet-terminal-p1"].ViewObject.Visibility) self.assertFalse(indexed["qet-terminal-p2"].ViewObject.Visibility) def test_route_eplan_connections_rebinds_local_template_terminals_from_wire_endpoints(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") root.addObject(device) terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-a") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-a") terminal_objects.ensure_string_property(device, "QetProjectUuid", "QET Exchange", "", "project-1") terminal_group = terminal_objects.ensure_terminal_group( doc, device, project_uuid="project-1", instance_id="instance-a", ) for slot_name, point in ( ("P1", app.Vector(0, 0, 0)), ("P2", app.Vector(100, 0, 0)), ): terminal = terminal_objects.create_lcs_object( doc, "QETTerminal_instance_a_{0}".format(slot_name), placement=app.Placement(point, app.Rotation()), label=slot_name, ) terminal_group.addObject(terminal) terminal_objects.set_terminal_semantics( terminal, "project-1", "device-a", "local:instance-a:{0}".format(slot_name), "instance-a", label=slot_name, slot_name=slot_name, ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_element_uuid": "device-a", "start_instance_id": "instance-a", "start_terminal_uuid": "qet-terminal-p1", "start_terminal_display": "P1", "end_element_uuid": "device-a", "end_instance_id": "instance-a", "end_terminal_uuid": "qet-terminal-p2", "end_terminal_display": "P2", } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, ) indexed = auto_routing.index_terminals(doc) self.assertEqual(1, report["routed"]) self.assertEqual(2, report["auto_bound_terminals"]) self.assertEqual(0, report["local_terminals"]) self.assertIn("qet-terminal-p1", indexed) self.assertIn("qet-terminal-p2", indexed) self.assertEqual("qet", indexed["qet-terminal-p1"].QetTerminalBindingMode) self.assertFalse(indexed["qet-terminal-p1"].ViewObject.Visibility) self.assertFalse(indexed["qet-terminal-p2"].ViewObject.Visibility) def test_clear_route_carriers_keeps_routed_wires(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", ) wire = auto_routing.route_eplan_connection_between_terminals(doc, start, end)["wire"] removed = routing_network.clear_route_carriers(doc) self.assertEqual(1, removed) self.assertEqual([], routing_network.collect_route_carriers(doc)) self.assertIn(wire, wiring_objects.ensure_routed_group(doc, "project-1").Group) if __name__ == "__main__": unittest.main()