import importlib import json import os import sqlite3 import sys import tempfile 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, wires=None): self.BoundBox = bbox self.Edges = edges or [] self.Faces = faces or [] self.Wires = wires 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 FakeCurveEdge: ShapeType = "Edge" def __init__(self, points): self._points = list(points) self.Vertexes = [FakeVertex(self._points[0]), FakeVertex(self._points[-1])] def discretize(self, *args, **kwargs): return list(self._points) class FakeWire: ShapeType = "Wire" def __init__(self, points, edges=None): self._points = list(points) self.Edges = edges or [] def discretize(self, *args, **kwargs): return list(self._points) 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.assertEqual("N4111: terminal-start -> terminal-end (Routed)", wire.Label) self.assertEqual(wire.Label, result["wire_object_label"]) 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_eplan_connection_route_applies_wire_style_from_wire_properties_database(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL, diameter_mm REAL, area_or_spec TEXT ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width, diameter_mm, area_or_spec) VALUES (42, 'project-1', '蓝色控制线', '#3366CC', 3.5, 1.25, '1.25mm2') """ ) connection.commit() finally: connection.close() result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-1", wire_label="N4111", options={ "wire_style_id": "42", "wire_style_database_path": str(db_path), }, ) wire = result["wire"] style = json.loads(wire.QetWireStyleJson) diagnostics = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual((0.2, 0.4, 0.8), wire.ViewObject.LineColor) self.assertEqual(3.5, wire.ViewObject.LineWidth) self.assertEqual("Resolved", wire.QetWireStyleStatus) self.assertEqual("蓝色控制线", wire.QetWireStyleName) self.assertEqual("1.25mm2", wire.QetWireSpecText) self.assertEqual("#3366CC", wire.QetWireColorText) self.assertEqual("42", style["id"]) self.assertEqual("蓝色控制线", style["name"]) self.assertEqual("#3366CC", style["line_color"]) self.assertEqual(3.5, style["line_width"]) self.assertEqual("1.25mm2", style["area_or_spec"]) self.assertEqual("Resolved", diagnostics["wire_style_status"]) self.assertEqual(style, diagnostics["wire_style"]) def test_eplan_connection_route_reports_missing_wire_style(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-missing-style", options={ "wire_style_id": "404", "wire_style_lookup": lambda _style_id, _project_uuid: {}, }, ) wire = result["wire"] diagnostics = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual("404", wire.QetWireStyleId) self.assertEqual("Missing", wire.QetWireStyleStatus) self.assertEqual("Missing", diagnostics["wire_style_status"]) self.assertNotIn("wire_style", diagnostics) self.assertEqual((0.0, 0.35, 1.0), wire.ViewObject.LineColor) self.assertEqual(5.0, wire.ViewObject.LineWidth) def test_eplan_connection_route_uses_wire_properties_database_from_environment(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", ) previous = os.environ.get("QET_WIRE_PROPERTIES_DB") with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width) VALUES (7, 'project-1', '红色动力线', 'rgb(255,0,0)', 6.0) """ ) connection.commit() finally: connection.close() os.environ["QET_WIRE_PROPERTIES_DB"] = str(db_path) try: result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="wire-env-style", options={"wire_style_id": "7"}, ) finally: if previous is None: os.environ.pop("QET_WIRE_PROPERTIES_DB", None) else: os.environ["QET_WIRE_PROPERTIES_DB"] = previous wire = result["wire"] self.assertEqual((1.0, 0.0, 0.0), wire.ViewObject.LineColor) self.assertEqual(6.0, wire.ViewObject.LineWidth) self.assertEqual("红色动力线", json.loads(wire.QetWireStyleJson)["name"]) def test_eplan_connection_route_applies_wire_style_line_type(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_type TEXT, line_width REAL ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_type, line_width) VALUES (8, 'project-1', '虚线样式', '#00AA00', 'DashLine', 2.5) """ ) connection.commit() finally: connection.close() result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "wire_style_id": "8", "wire_style_database_path": str(db_path), }, ) wire = result["wire"] style = json.loads(wire.QetWireStyleJson) self.assertEqual("Dashed", wire.ViewObject.DrawStyle) self.assertEqual("DashLine", style["line_type"]) def test_eplan_connection_route_accepts_bare_hex_color_and_diameter_width(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL, diameter_mm REAL ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width, diameter_mm) VALUES (12, 'project-1', '无井号十六进制颜色', '3366CC', NULL, 2.25), (13, 'project-1', '0x十六进制颜色', '0x3366CC', NULL, 1.5) """ ) connection.commit() finally: connection.close() result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "wire_style_id": "12", "wire_style_database_path": str(db_path), }, ) wire = result["wire"] self.assertEqual((0.2, 0.4, 0.8), wire.ViewObject.LineColor) self.assertEqual(2.25, wire.ViewObject.LineWidth) second_result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "wire_style_id": "13", "wire_style_database_path": str(db_path), }, ) second_wire = second_result["wire"] self.assertEqual((0.2, 0.4, 0.8), second_wire.ViewObject.LineColor) self.assertEqual(1.5, second_wire.ViewObject.LineWidth) def test_eplan_connection_route_estimates_width_from_wire_spec_area(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL, diameter_mm REAL, area_or_spec TEXT ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width, diameter_mm, area_or_spec) VALUES (14, 'project-1', '按截面积估算', '#AA0000', NULL, NULL, '2.5mm2') """ ) connection.commit() finally: connection.close() result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "wire_style_id": "14", "wire_style_database_path": str(db_path), }, ) wire = result["wire"] self.assertAlmostEqual(1.784, wire.ViewObject.LineWidth, places=3) self.assertEqual("2.5mm2", wire.QetWireSpecText) def test_eplan_connection_route_accepts_argb_hex_color(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width) VALUES (15, 'project-1', 'ARGB颜色', '#FF3366CC', 2.0) """ ) connection.commit() finally: connection.close() result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "wire_style_id": "15", "wire_style_database_path": str(db_path), }, ) wire = result["wire"] self.assertEqual((0.2, 0.4, 0.8), wire.ViewObject.LineColor) self.assertEqual("#FF3366CC", wire.QetWireColorText) def test_eplan_connection_route_accepts_decimal_integer_color(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width) VALUES (16, 'project-1', '十进制颜色', '16711680', 2.0) """ ) connection.commit() finally: connection.close() result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "wire_style_id": "16", "wire_style_database_path": str(db_path), }, ) wire = result["wire"] self.assertEqual((1.0, 0.0, 0.0), wire.ViewObject.LineColor) self.assertEqual("16711680", wire.QetWireColorText) def test_route_eplan_connections_reuses_wire_style_lookup_for_same_style_id(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, 40, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 40, 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, 40, 20), app.Vector(100, 40, 20)], project_uuid="project-1", kind="WireDuct", ) calls = [] def lookup(style_id, project_uuid): calls.append((style_id, project_uuid)) return {"id": style_id, "name": "缓存样式", "line_color": "#00AA00", "line_width": 4.0} payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", "wire_style_id": 9, }, { "wire_id": "wire-b", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", "wire_style_id": 9, }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"wire_style_lookup": lookup}, ) self.assertEqual(2, report["routed"]) self.assertEqual([("9", "project-1")], calls) def test_route_eplan_connections_reports_wire_style_status_counts(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, 40, 0)) _terminal(doc, terminal_objects, "TerminalEndB", "terminal-end-b", app.Vector(100, 40, 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, 40, 20), app.Vector(100, 40, 20)], project_uuid="project-1", kind="WireDuct", ) def lookup(style_id, _project_uuid): if style_id == "1": return {"id": "1", "name": "绿色控制线", "line_color": "#00AA00"} return {} payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-resolved-style", "wire_label": "N1", "start_terminal_uuid": "terminal-start-a", "end_terminal_uuid": "terminal-end-a", "wire_style_id": 1, }, { "wire_id": "wire-missing-style", "wire_label": "N2", "start_terminal_uuid": "terminal-start-b", "end_terminal_uuid": "terminal-end-b", "wire_style_id": 404, }, ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"wire_style_lookup": lookup}, ) compact = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(2, report["routed"]) self.assertEqual({"Resolved": 1, "Missing": 1}, report["wire_style_status_counts"]) self.assertEqual("Resolved", report["routes"][0]["wire_style_status"]) self.assertEqual("绿色控制线", report["routes"][0]["wire_style"]["name"]) self.assertEqual("Missing", report["routes"][1]["wire_style_status"]) self.assertEqual({"Resolved": 1, "Missing": 1}, compact["wire_style_status_counts"]) self.assertEqual("绿色控制线", compact["route_samples"][0]["wire_style"]["name"]) self.assertEqual("Missing", compact["route_samples"][1]["wire_style_status"]) self.assertEqual("404", compact["route_samples"][1]["wire_style_id"]) self.assertEqual(1, compact["missing_wire_style_samples_count"]) self.assertEqual("N2", compact["missing_wire_style_samples"][0]["wire_label"]) self.assertEqual("404", compact["missing_wire_style_samples"][0]["wire_style_id"]) self.assertIn("导线样式:缺失 1 条", message) self.assertIn("示例导线 N2 样式 404", message) def test_route_eplan_connections_uses_wire_style_database_path_from_payload(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.sqlite" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT NOT NULL, name TEXT, line_color TEXT, line_width REAL ) """ ) connection.execute( """ INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width) VALUES (11, 'project-1', 'payload样式', '#8844FF', 4.5) """ ) connection.commit() finally: connection.close() payload = { "project_uuid": "project-1", "wire_style_database_path": str(db_path), "wires": [ { "wire_id": "wire-payload-style", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": 11, } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) routed_group = doc.getObject("QETWiring_04_Routed") wire = list(getattr(routed_group, "Group", []) or [])[0] compact = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["routed"]) self.assertEqual(str(db_path), report["wire_style_database_path"]) self.assertEqual(str(db_path), compact["wire_style_database_path"]) self.assertIn("导线样式库:{0}".format(str(db_path)), message) self.assertEqual((0.533333, 0.266667, 1.0), wire.ViewObject.LineColor) self.assertEqual(4.5, wire.ViewObject.LineWidth) self.assertEqual("payload样式", json.loads(wire.QetWireStyleJson)["name"]) def test_route_eplan_connections_uses_fallback_style_database_when_payload_database_is_empty(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", ) with tempfile.TemporaryDirectory() as temp_dir: wrong_dir = Path(temp_dir) / "wrong" / "datafiles" right_dir = Path(temp_dir) / "right" / "datafiles" exchange_dir = Path(temp_dir) / "right" / ".qet_freecad" wrong_dir.mkdir(parents=True) right_dir.mkdir(parents=True) exchange_dir.mkdir(parents=True) wrong_db = wrong_dir / "project-local.db" right_db = right_dir / "project-local.db" for db_path, rows in ( (wrong_db, []), (right_db, [(1, "project-1", "红色动力线", "#ff0000", 4.0)]), ): connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT, name TEXT, line_color TEXT, line_width REAL ) """ ) connection.executemany( "INSERT INTO wire_properties (id, project_uuid, name, line_color, line_width) VALUES (?, ?, ?, ?, ?)", rows, ) connection.commit() finally: connection.close() json_path = exchange_dir / "2d_to_3d.json" json_path.write_text( json.dumps({"project_uuid": "project-1", "wires": []}), encoding="utf-8", ) app._qet_exchange_summary = {"json_path": str(json_path)} payload = { "project_uuid": "project-1", "wire_style_database_path": str(wrong_db), "wires": [ { "wire_id": "wire-1", "wire_label": "N1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "1", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) routed_group = doc.getObject("QETWiring_04_Routed") wire = list(getattr(routed_group, "Group", []) or [])[0] compact = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, report["routed"]) self.assertEqual(str(right_db), report["wire_style_database_path"]) self.assertEqual(str(wrong_db), report["wire_style_database_fallback_from"]) self.assertEqual(str(wrong_db), compact["wire_style_database_fallback_from"]) self.assertIn("从备用库恢复", message) self.assertIn(str(wrong_db), message) self.assertEqual((1.0, 0.0, 0.0), wire.ViewObject.LineColor) self.assertEqual(4.0, wire.ViewObject.LineWidth) self.assertEqual("红色动力线", json.loads(wire.QetWireStyleJson)["name"]) 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_routed_wire_exposes_route_source_labels_for_manual_inspection(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)) carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="主线槽A", project_uuid="project-1", kind="WireDuct", ) carrier.QetRouteSourceLabel = "黄色主路径" carrier.QetRouteSourcePathIndex = "2" result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) wire = result["wire"] self.assertEqual("黄色主路径(路径2)", wire.QetRouteSourceLabels) self.assertEqual("QETRouteCarrier", wire.QetRouteCarrierNames) payload = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual(["黄色主路径(路径2)"], payload["route_source_labels"]) self.assertEqual(["QETRouteCarrier"], payload["route_carrier_names"]) def test_route_track_preserves_user_path_source_path_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)) user_path = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="用户路径B", project_uuid="project-1", kind="UserPath", ) user_path.QetRouteSourceName = "MultiWireRouteSketch" user_path.QetRouteSourceLabel = "多路径草图" user_path.QetRouteSourceKind = "UserPath" user_path.QetRouteSourcePathIndex = "2" result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) route_track = json.loads(result["wire"].QetRouteTrackJson) user_path_carriers = [ segment["carrier"] for segment in route_track["segments"] if segment["carrier"]["kind"] == "UserPath" ] self.assertTrue(user_path_carriers) self.assertEqual("2", user_path_carriers[0].get("source_path_index")) def test_carrier_payload_preserves_source_metadata(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="用户路径B", project_uuid="project-1", kind="UserPath", ) carrier.QetRouteSourceName = "MultiWireRouteSketch" carrier.QetRouteSourceLabel = "多路径草图" carrier.QetRouteSourceKind = "UserPath" carrier.QetRouteSourcePathIndex = "2" payload = routing_network.carrier_payload(carrier) self.assertEqual("MultiWireRouteSketch", payload["source_name"]) self.assertEqual("多路径草图", payload["source_label"]) self.assertEqual("UserPath", payload["source_kind"]) self.assertEqual("2", payload["source_path_index"]) 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)], label="主线槽A", 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("network-dijkstra-v1", result["algorithm"]) 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_includes_cabinet_side_cover(self): _install_fake_freecad() terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") side_cover = doc.addObject("Part::Feature", "SideCover") side_cover.Label = "SIDE COVER-1_P00" side_cover.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, ) self.assertGreater(len(created), 0) self.assertEqual("RoutingRange", side_cover.QetRoutingSourceKind) self.assertEqual("SupportSurface", side_cover.QetRoutingObstacleMode) def test_support_surface_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") panel = doc.addObject("Part::Feature", "MountingPlateA") panel.Label = "安装板A" panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) panel.QetRouteCarrierCapacity = 6 created = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) self.assertGreater(len(created), 0) self.assertTrue(all(carrier.QetRouteCarrierCapacity == 6 for carrier in created)) def test_cabinet_boundary_objects_are_not_route_sources(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") boundary_duct = doc.addObject("Part::Feature", "BoundaryWireDuct") boundary_duct.Label = "Wire Duct Boundary" boundary_duct.Shape = FakeShape(FakeBoundBox(0, 160, -10, 10, 0, 20)) boundary_duct.QetRoutingBoundaryKind = "CabinetInterior" boundary_panel = doc.addObject("Part::Feature", "BoundaryMountingPlate") boundary_panel.Label = "安装板边界" boundary_panel.Shape = FakeShape(FakeBoundBox(0, 120, 0, 5, 0, 100)) boundary_panel.QetRoutingBoundaryKind = "CabinetInterior" boundary_path = doc.addObject("Part::Feature", "BoundaryPointPath") boundary_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] boundary_path.QetRoutingBoundaryKind = "CabinetInterior" wire_ducts = routing_network.create_wire_duct_carriers_from_document( doc, project_uuid="project-1", ) surfaces = routing_network.create_surface_carriers_from_document( doc, project_uuid="project-1", spacing=60.0, offset=5.0, margin=0.0, ) user_paths = routing_network.create_user_path_carriers_from_selection( doc, [FakeSelectionItem(obj=boundary_path)], project_uuid="project-1", ) self.assertEqual([], wire_ducts) self.assertEqual([], surfaces) self.assertEqual([], user_paths) self.assertEqual([], routing_network.collect_route_carriers(doc)) def test_detect_user_path_sources_skips_origin_axes_with_huge_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") axis = doc.addObject("App::Line", "X_Axis") axis.Label = "X轴" axis.Shape = FakeShape( FakeBoundBox(-1e100, 1e100, 0, 0, 0, 0), edges=[FakeEdge(app.Vector(0, 0, 0), app.Vector(1, 0, 0))], ) route_path = doc.addObject("Part::FeaturePython", "UserRoutePath") route_path.Points = [app.Vector(0, 0, 20), app.Vector(100, 0, 20)] sources = routing_network.detect_user_path_sources(doc) self.assertEqual(["UserRoutePath"], [source.Name for source in sources]) def test_detect_user_path_sources_skips_existing_route_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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) sources = routing_network.detect_user_path_sources(doc) self.assertEqual([], sources) 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_controller_creates_user_path_bridge_from_selected_route_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 = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="线槽", ) main_path = routing_network.create_route_carrier( doc, [app.Vector(120, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", kind="RoutingRange", label="主网络", ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=duct), FakeSelectionItem(obj=main_path)], ) result = auto_routing_panel.AutoRoutingController().create_user_path_bridge_from_selection() self.assertEqual(1, result["user_path_bridges"]) self.assertEqual(1, result["network"]["kinds"]["UserPath"]) def test_controller_creates_user_path_bridge_from_path_network_diagnostic_suggestion(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, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="孤立线槽", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", kind="TerminalAccess", label="端子接入", ) result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() carriers = routing_network.collect_route_carriers(doc) user_paths = [carrier for carrier in carriers if carrier.QetRouteCarrierKind == "UserPath"] self.assertEqual(1, result["user_path_bridges"]) self.assertEqual(1, result["diagnostic_suggestions"]) self.assertEqual(1, len(user_paths)) self.assertEqual( [(100.0, 0.0, 0.0), (1000.0, 0.0, 0.0)], [(point.x, point.y, point.z) for point in user_paths[0].Points], ) def test_controller_creates_user_path_bridge_from_main_path_detour_pair(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") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") fallback_source.QetRouteSourceLabel = "门板布线面" current_source = doc.addObject("Part::Feature", "MainUserPathSource") current_source.QetRouteSourceLabel = "主路径A" fallback_carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="RoutingRange", label="门板布线面 carrier", ) current_carrier = routing_network.create_route_carrier( doc, [app.Vector(140, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", kind="UserPath", label="主路径A carrier", ) fallback_carrier.QetRouteSourceName = fallback_source.Name fallback_carrier.QetRouteSourceLabel = "门板布线面" current_carrier.QetRouteSourceName = current_source.Name current_carrier.QetRouteSourceLabel = "主路径A" wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_pair_bridge") wire.Label = "N-PAIR-BRIDGE: A1 -> B1" wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-main-path-pair-bridge" wire.QetRouteIssueCodes = "main_path_detour_missing" wire.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", "QetRouteTrackJson", ] wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["RoutingRange"], "rejected_fallback_labels": ["门板布线面"], } }, ensure_ascii=False, ) wire.QetRouteTrackJson = json.dumps( { "segments": [ {"carrier": {"kind": "UserPath", "source_label": "主路径A"}} ] }, ensure_ascii=False, ) routed_group.addObject(wire) result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() user_paths = [ carrier for carrier in routing_network.collect_route_carriers(doc) if carrier.QetRouteCarrierKind == "UserPath" ] created_bridges = [ carrier for carrier in user_paths if "QET User Bridge" in str(getattr(carrier, "Label", "") or "") ] self.assertEqual(1, result["main_path_detour_bridge_pairs"]) self.assertEqual(1, result["main_path_detour_user_path_bridges"]) self.assertEqual(0, result["main_path_detour_bridge_duplicates"]) self.assertEqual(1, len(created_bridges)) self.assertEqual("门板布线面 -> 主路径A", created_bridges[0].QetRouteBridgePairLabel) self.assertEqual( [(100.0, 0.0, 0.0), (140.0, 20.0, 0.0)], [(point.x, point.y, point.z) for point in created_bridges[0].Points], ) second = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() self.assertEqual(0, second["main_path_detour_user_path_bridges"]) self.assertEqual(1, second["main_path_detour_bridge_duplicates"]) def test_controller_does_not_duplicate_diagnostic_user_path_bridge(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, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", kind="TerminalAccess", ) controller = auto_routing_panel.AutoRoutingController() first = controller.create_user_path_bridges_from_diagnostic_suggestions() second = controller.create_user_path_bridges_from_diagnostic_suggestions() user_paths = [ carrier for carrier in routing_network.collect_route_carriers(doc) if carrier.QetRouteCarrierKind == "UserPath" ] self.assertEqual(1, first["user_path_bridges"]) self.assertEqual(0, second["user_path_bridges"]) self.assertEqual(0, second["diagnostic_suggestions"]) self.assertEqual(1, len(user_paths)) def test_route_eplan_connections_auto_creates_diagnostic_user_path_bridge(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="孤立线槽", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", kind="TerminalAccess", label="端子接入", ) report = auto_routing.route_eplan_connections( doc, payload={"project_uuid": "project-1", "wires": []}, options={"auto_create_diagnostic_bridges": True}, project_uuid="project-1", update_network=False, ) message = auto_routing.format_eplan_connection_route_report(report) user_paths = [ carrier for carrier in routing_network.collect_route_carriers(doc) if carrier.QetRouteCarrierKind == "UserPath" ] self.assertEqual(1, report["auto_diagnostic_bridges"]["created_count"]) self.assertEqual(1, len(user_paths)) self.assertNotIn( "wire_ducts_without_terminal_access", report["routing_path_network_diagnostic"]["issue_codes"], ) self.assertIn("自动诊断桥接:生成 UserPath 1 条", message) def test_route_eplan_connections_does_not_auto_create_diagnostic_bridge_by_default(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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="孤立线槽", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", kind="TerminalAccess", label="端子接入", ) report = auto_routing.route_eplan_connections( doc, payload={"project_uuid": "project-1", "wires": []}, project_uuid="project-1", update_network=False, ) user_paths = [ carrier for carrier in routing_network.collect_route_carriers(doc) if carrier.QetRouteCarrierKind == "UserPath" ] self.assertFalse(report["auto_diagnostic_bridges"]["enabled"]) self.assertEqual(0, report["auto_diagnostic_bridges"]["created_count"]) self.assertEqual(0, len(user_paths)) def test_controller_repeats_diagnostic_bridge_until_no_new_bridge_is_created(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") original_check = auto_routing.check_eplan_routing_path_network original_create = routing_network.create_user_path_bridges_from_diagnostic_suggestions calls = {"check": 0, "create": 0} def fake_check(_doc, project_uuid="", options=None): calls["check"] += 1 return {"diagnostic": {"pass_index": calls["check"]}} def fake_create(_doc, diagnostic, project_uuid=""): calls["create"] += 1 if int(diagnostic.get("pass_index", 0) or 0) <= 2: return {"suggestions": 1, "created": [object()], "duplicates": 0, "stale_suggestions": 0} return {"suggestions": 0, "created": [], "duplicates": 0, "stale_suggestions": 0} try: auto_routing.check_eplan_routing_path_network = fake_check routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create result = auto_routing_panel.AutoRoutingController().create_user_path_bridges_from_diagnostic_suggestions() finally: auto_routing.check_eplan_routing_path_network = original_check routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create self.assertEqual(2, result["user_path_bridges"]) self.assertEqual(2, result["diagnostic_suggestions"]) self.assertEqual(3, result["diagnostic_passes"]) self.assertEqual(3, calls["check"]) self.assertEqual(3, calls["create"]) def test_selected_curve_edges_are_discretized_as_user_path_polyline(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("Sketcher::SketchObject", "CurvedRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 60, 20, 20), edges=[ FakeCurveEdge( [ app.Vector(0, 0, 20), app.Vector(25, 40, 20), app.Vector(75, 40, 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( [(0.0, 0.0, 20.0), (25.0, 40.0, 20.0), (75.0, 40.0, 20.0), (100.0, 0.0, 20.0)], [(point.x, point.y, point.z) for point in carriers[0].Points], ) def test_selected_shape_wires_are_used_as_user_path_polyline(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("Sketcher::SketchObject", "WireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire( [ app.Vector(0, 0, 20), app.Vector(0, 60, 20), app.Vector(60, 80, 20), app.Vector(120, 80, 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), (0.0, 60.0, 20.0), (60.0, 80.0, 20.0), (120.0, 80.0, 20.0)], [(point.x, point.y, point.z) for point in carriers[0].Points], ) def test_selected_user_path_shape_points_honor_object_placement(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("Sketcher::SketchObject", "MovedRouteSketch") route_path.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation()) route_path.Shape = FakeShape( FakeBoundBox(0, 50, 0, 50, 20, 20), wires=[ FakeWire( [ app.Vector(0, 0, 20), app.Vector(50, 0, 20), app.Vector(50, 50, 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( [(100.0, 10.0, 25.0), (150.0, 10.0, 25.0), (150.0, 60.0, 25.0)], [(point.x, point.y, point.z) for point in carriers[0].Points], ) def test_disconnected_shape_wires_create_separate_user_paths(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("Sketcher::SketchObject", "MultiWireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 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(2, result["user_path_carriers"]) self.assertEqual(2, len(carriers)) self.assertEqual( [ [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0)], [(80.0, 80.0, 20.0), (120.0, 80.0, 20.0)], ], [[(point.x, point.y, point.z) for point in carrier.Points] for carrier in carriers], ) self.assertEqual(["1", "2"], [carrier.QetRouteSourcePathIndex for carrier in carriers]) self.assertEqual(2, len(json.loads(route_path.QetRouteCarrierNamesJson))) def test_refreshing_multi_wire_user_path_removes_stale_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") route_path = doc.addObject("Sketcher::SketchObject", "EditableMultiWireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), ], ) selection = [FakeSelectionItem(obj=route_path)] first = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) route_path.Shape = FakeShape( FakeBoundBox(0, 60, 0, 20, 20, 20), wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(60, 0, 20)])], ) second = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) carriers = routing_network.collect_route_carriers(doc) self.assertEqual(2, len(first)) self.assertEqual(1, len(second)) self.assertEqual(1, len(carriers)) self.assertEqual([(0.0, 0.0, 20.0), (60.0, 0.0, 20.0)], [(p.x, p.y, p.z) for p in carriers[0].Points]) self.assertEqual("", carriers[0].QetRouteSourcePathIndex) self.assertEqual(1, len(json.loads(route_path.QetRouteCarrierNamesJson))) def test_refreshing_single_wire_user_path_adds_new_carriers_for_added_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") route_path = doc.addObject("Sketcher::SketchObject", "GrowingMultiWireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 60, 0, 20, 20, 20), wires=[FakeWire([app.Vector(0, 0, 20), app.Vector(60, 0, 20)])], ) selection = [FakeSelectionItem(obj=route_path)] first = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(60, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), ], ) second = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) carriers = routing_network.collect_route_carriers(doc) self.assertEqual(1, len(first)) self.assertEqual(2, len(second)) self.assertEqual(2, len(carriers)) self.assertEqual(["1", "2"], [carrier.QetRouteSourcePathIndex for carrier in carriers]) self.assertEqual(2, len(json.loads(route_path.QetRouteCarrierNamesJson))) def test_controller_marks_selected_object_as_cabinet_interior_boundary(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") cabinet_space = doc.addObject("Part::Feature", "CabinetInteriorSpace") cabinet_space.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=cabinet_space)], ) result = auto_routing_panel.AutoRoutingController().mark_cabinet_boundary_from_selection() self.assertEqual(1, result["cabinet_boundary_objects"]) self.assertEqual("CabinetInterior", cabinet_space.QetRoutingBoundaryKind) self.assertEqual("RoutingBoundary", cabinet_space.QetRoutingRole) self.assertEqual("PassThrough", cabinet_space.QetRoutingObstacleMode) self.assertEqual(1, len(auto_routing.collect_routing_boundaries(doc))) def test_controller_marks_selected_object_obstacle_pass_through_and_restores(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") obstacle = doc.addObject("Part::Feature", "BracketObstacle") obstacle.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=obstacle)], ) controller = auto_routing_panel.AutoRoutingController() ignored = controller.mark_selected_objects_pass_through_obstacle() self.assertEqual([], auto_routing.collect_obstacles(doc)) restored = controller.restore_selected_objects_as_obstacles() self.assertEqual(1, ignored["obstacle_mode_objects"]) self.assertEqual(1, restored["obstacle_mode_objects"]) self.assertEqual("", obstacle.QetRoutingObstacleMode) self.assertEqual(["BracketObstacle"], [item["name"] for item in auto_routing.collect_obstacles(doc)]) def test_controller_summary_reports_pass_through_obstacle_count(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") bracket = doc.addObject("Part::Feature", "BracketObstacle") bracket.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) bracket.QetRoutingObstacleMode = "PassThrough" message = auto_routing_panel.AutoRoutingController().summary() self.assertIn("忽略碰撞:1", message) def test_controller_selects_top_collision_obstacles_from_latest_batch_diagnostic(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") obstacle = doc.addObject("Part::Feature", "Compound053") obstacle.Label = "NAUO118" other = doc.addObject("Part::Feature", "Compound039") other.Label = "NAUO141" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "top_collision_obstacles": [ {"label": "NAUO118", "name": "Compound053", "count": 18}, {"label": "NAUO141", "name": "Compound039", "count": 6}, {"label": "NAUO404", "name": "Compound404", "count": 1}, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_top_collision_obstacles() self.assertEqual(2, result["selected_collision_obstacles"]) self.assertEqual(["Compound053", "Compound039"], result["selected_collision_obstacle_names"]) self.assertEqual(["Compound404"], result["missing_collision_obstacle_names"]) self.assertEqual([obstacle, other], selected) def test_controller_selects_top_collision_parent_assemblies_from_latest_batch_diagnostic(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") door = doc.addObject("App::LinkGroup", "DoorAssembly") door.Label = "FRONT DOOR-R ASS'Y" cabinet = doc.addObject("App::LinkGroup", "CabinetAssembly") cabinet.Label = "CABINET ASS'Y" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "top_collision_obstacles": [ { "label": "NAUO141", "name": "Compound039", "count": 6, "parent_names": ["DoorAssembly"], "parent_labels": ["FRONT DOOR-R ASS'Y"], }, { "label": "NAUO142", "name": "Compound040", "count": 3, "parent_names": ["DoorAssembly"], "parent_labels": ["FRONT DOOR-R ASS'Y"], }, { "label": "NAUO118", "name": "Compound053", "count": 18, "parent_names": ["CabinetAssembly"], "parent_labels": ["CABINET ASS'Y"], }, { "label": "NAUO404", "name": "Compound404", "count": 1, "parent_names": ["MissingAssembly"], "parent_labels": ["MISSING ASS'Y"], }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_top_collision_parent_assemblies() self.assertEqual(2, result["selected_collision_parent_assemblies"]) self.assertEqual(["DoorAssembly", "CabinetAssembly"], result["selected_collision_parent_assembly_names"]) self.assertEqual(["MissingAssembly"], result["missing_collision_parent_assembly_refs"]) self.assertEqual([door, cabinet], selected) def test_controller_selects_only_structural_collision_parent_assemblies(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") door = doc.addObject("App::LinkGroup", "DoorAssembly") door.Label = "FRONT DOOR-R ASS'Y" device_assembly = doc.addObject("App::LinkGroup", "DeviceAssembly") device_assembly.Label = "DEVICE ASS'Y" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "top_collision_obstacles": [ { "label": "NAUO141", "name": "Compound039", "count": 6, "parent_names": ["DoorAssembly"], "parent_labels": ["FRONT DOOR-R ASS'Y"], "resolution_hint_code": "review_pass_through_structural_obstacle", }, { "label": "ID:12", "name": "QETDevice_A", "count": 3, "parent_names": ["DeviceAssembly"], "parent_labels": ["DEVICE ASS'Y"], "resolution_hint_code": "review_device_or_layout_collision", }, { "label": "支架缺失", "name": "MissingBracket", "count": 1, "parent_names": ["MissingStructure"], "parent_labels": ["MISSING STRUCTURE"], "resolution_hint_code": "review_pass_through_structural_obstacle", }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_structural_collision_parent_assemblies() self.assertEqual(1, result["selected_structural_collision_parent_assemblies"]) self.assertEqual(["DoorAssembly"], result["selected_structural_collision_parent_assembly_names"]) self.assertEqual(["MissingStructure"], result["missing_structural_collision_parent_assembly_refs"]) self.assertEqual([door], selected) self.assertNotIn(device_assembly, selected) def test_controller_marks_only_structural_collision_parent_assemblies_pass_through(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") door = doc.addObject("App::LinkGroup", "DoorAssembly") door.Label = "FRONT DOOR-R ASS'Y" device_assembly = doc.addObject("App::LinkGroup", "DeviceAssembly") device_assembly.Label = "DEVICE ASS'Y" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "top_collision_obstacles": [ { "label": "NAUO141", "name": "Compound039", "count": 6, "parent_names": ["DoorAssembly"], "parent_labels": ["FRONT DOOR-R ASS'Y"], "resolution_hint_code": "review_pass_through_structural_obstacle", }, { "label": "ID:12", "name": "QETDevice_A", "count": 3, "parent_names": ["DeviceAssembly"], "parent_labels": ["DEVICE ASS'Y"], "resolution_hint_code": "review_device_or_layout_collision", }, { "label": "支架缺失", "name": "MissingBracket", "count": 1, "parent_names": ["MissingStructure"], "parent_labels": ["MISSING STRUCTURE"], "resolution_hint_code": "review_pass_through_structural_obstacle", }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) result = auto_routing_panel.AutoRoutingController().mark_structural_collision_parent_assemblies_pass_through() self.assertEqual(1, result["marked_structural_collision_parent_assemblies"]) self.assertEqual(["DoorAssembly"], result["marked_structural_collision_parent_assembly_names"]) self.assertEqual(["MissingStructure"], result["missing_structural_collision_parent_assembly_refs"]) self.assertEqual("PassThrough", door.QetRoutingObstacleMode) self.assertEqual("", getattr(device_assembly, "QetRoutingObstacleMode", "")) def test_controller_marks_nearest_structural_parent_without_broad_root_group(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") door = doc.addObject("App::LinkGroup", "DoorAssembly") door.Label = "FRONT DOOR-R ASS'Y" cabinet = doc.addObject("App::LinkGroup", "CabinetAssembly") cabinet.Label = "MCCB CABINET ASS'Y" project_root = doc.addObject("App::DocumentObjectGroup", "QETExchangeDevices") project_root.Label = "QET Exchange Devices" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "top_collision_obstacles": [ { "label": "NAUO141", "name": "Compound039", "count": 6, "parent_names": [ "DoorAssembly", "CabinetAssembly", "QETExchangeDevices", ], "parent_labels": [ "FRONT DOOR-R ASS'Y", "MCCB CABINET ASS'Y", "QET Exchange Devices", ], "resolution_hint_code": "review_pass_through_structural_obstacle", }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) result = auto_routing_panel.AutoRoutingController().mark_structural_collision_parent_assemblies_pass_through() self.assertEqual(1, result["marked_structural_collision_parent_assemblies"]) self.assertEqual(["DoorAssembly"], result["marked_structural_collision_parent_assembly_names"]) self.assertEqual("PassThrough", door.QetRoutingObstacleMode) self.assertEqual("", getattr(cabinet, "QetRoutingObstacleMode", "")) self.assertEqual("", getattr(project_root, "QetRoutingObstacleMode", "")) def test_controller_selects_only_device_or_layout_collision_obstacles(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") device = doc.addObject("Part::Feature", "QETDevice_A") device.Label = "ID:12" bracket = doc.addObject("Part::Feature", "Bracket_A") bracket.Label = "NAUO141" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "top_collision_obstacles": [ { "label": "ID:12", "name": "QETDevice_A", "count": 2, "resolution_hint_code": "review_device_or_layout_collision", }, { "label": "NAUO141", "name": "Bracket_A", "count": 6, "resolution_hint_code": "review_pass_through_structural_obstacle", }, { "label": "缺失设备", "name": "MissingDevice", "count": 1, "resolution_hint_code": "review_device_or_layout_collision", }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_device_or_layout_collision_obstacles() self.assertEqual(1, result["selected_device_or_layout_collision_obstacles"]) self.assertEqual(["QETDevice_A"], result["selected_device_or_layout_collision_obstacle_names"]) self.assertEqual(["MissingDevice"], result["missing_device_or_layout_collision_obstacle_names"]) self.assertEqual([device], selected) self.assertNotIn(bracket, selected) def test_controller_selects_collision_wires_from_latest_batch_diagnostic(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") wire_a = doc.addObject("Part::Feature", "QETRoutedConnection_wire_a") wire_a.Label = "N-COL-A: A1 -> B1 (CollisionWarning)" wire_a.RouteType = "RoutedConnection" wire_a.QetWireUuid = "wire-a" wire_b = doc.addObject("Part::Feature", "QETRoutedConnection_wire_b") wire_b.Label = "N-COL-B: A2 -> B2 (CollisionWarning)" wire_b.RouteType = "RoutedConnection" wire_b.QetWireUuid = "wire-b" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "collision_samples": [ { "wire_uuid": "wire-a", "wire_object_label": "N-COL-A: A1 -> B1 (CollisionWarning)", }, { "wire_uuid": "wire-missing", "wire_object_label": "N-MISSING: A3 -> B3 (CollisionWarning)", }, ], "route_samples": [ { "wire_uuid": "wire-b", "wire_object_label": "N-COL-B: A2 -> B2 (CollisionWarning)", "issue_codes": ["collision_warnings"], } ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_collision_wires() self.assertEqual(2, result["selected_collision_wires"]) self.assertEqual( ["QETRoutedConnection_wire_a", "QETRoutedConnection_wire_b"], result["selected_collision_wire_names"], ) self.assertEqual(["wire-missing"], result["missing_collision_wire_refs"]) self.assertEqual([wire_a, wire_b], selected) def test_controller_selects_collision_wires_from_wire_object_issue_codes(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") collision_wire = doc.addObject("Part::Feature", "QETRoutedConnection_third_party") collision_wire.Label = "N-COL: A1 -> B1 (CollisionWarning)" collision_wire.RouteType = "RoutedConnection" collision_wire.QetWireUuid = "wire-third-party" collision_wire.QetRouteIssueCodes = "collision_warnings, third_party_device_collisions" normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") normal_wire.Label = "N-OK: A2 -> B2 (Routed)" normal_wire.RouteType = "RoutedConnection" normal_wire.QetWireUuid = "wire-ok" normal_wire.QetRouteIssueCodes = "" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( {"route_samples": []}, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_collision_wires() self.assertEqual(1, result["selected_collision_wires"]) self.assertEqual( ["QETRoutedConnection_third_party"], result["selected_collision_wire_names"], ) self.assertEqual([], result["missing_collision_wire_refs"]) self.assertEqual([collision_wire], selected) def test_controller_selects_issue_wires_from_latest_batch_diagnostic(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") long_wire = doc.addObject("Part::Feature", "QETRoutedConnection_long") long_wire.Label = "N-LONG: A1 -> B1 (Routed)" long_wire.RouteType = "RoutedConnection" long_wire.QetWireUuid = "wire-long" boundary_wire = doc.addObject("Part::Feature", "QETRoutedConnection_boundary") boundary_wire.Label = "N-BOUNDARY: A2 -> B2 (BoundaryWarning)" boundary_wire.RouteType = "RoutedConnection" boundary_wire.QetWireUuid = "wire-boundary" normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") normal_wire.Label = "N-OK: A3 -> B3 (Routed)" normal_wire.RouteType = "RoutedConnection" normal_wire.QetWireUuid = "wire-ok" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "route_samples": [ { "wire_uuid": "wire-long", "wire_object_label": "N-LONG: A1 -> B1 (Routed)", "issue_codes": ["long_terminal_access"], }, { "wire_uuid": "wire-boundary", "wire_object_label": "N-BOUNDARY: A2 -> B2 (BoundaryWarning)", "issue_codes": ["boundary_warning"], }, { "wire_uuid": "wire-ok", "wire_object_label": "N-OK: A3 -> B3 (Routed)", "issue_codes": [], }, { "wire_uuid": "wire-missing", "wire_object_label": "N-MISSING: A4 -> B4 (Routed)", "issue_codes": ["route_capacity_pressure"], }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_issue_wires() self.assertEqual(2, result["selected_issue_wires"]) self.assertEqual( ["QETRoutedConnection_long", "QETRoutedConnection_boundary"], result["selected_issue_wire_names"], ) self.assertEqual(["wire-missing"], result["missing_issue_wire_refs"]) self.assertEqual([long_wire, boundary_wire], selected) def test_controller_selects_main_path_detour_missing_wires(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") sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled") sampled_wire.Label = "N-MAINPATH-A: A1 -> B1 (MainPathDetourMissing)" sampled_wire.RouteType = "RoutedConnection" sampled_wire.QetWireUuid = "wire-sampled" object_issue_wire = doc.addObject("Part::Feature", "QETRoutedConnection_object_issue") object_issue_wire.Label = "N-MAINPATH-B: A2 -> B2 (MainPathDetourMissing)" object_issue_wire.RouteType = "RoutedConnection" object_issue_wire.QetWireUuid = "wire-object-issue" object_issue_wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") normal_wire.Label = "N-OK: A3 -> B3 (Routed)" normal_wire.RouteType = "RoutedConnection" normal_wire.QetWireUuid = "wire-ok" normal_wire.QetRouteIssueCodes = "collision_warnings" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "route_samples": [ { "wire_uuid": "wire-sampled", "wire_object_label": "N-MAINPATH-A: A1 -> B1 (MainPathDetourMissing)", "issue_codes": ["main_path_detour_missing"], }, { "wire_uuid": "wire-missing", "wire_object_label": "N-MISSING: A4 -> B4 (MainPathDetourMissing)", "issue_codes": ["main_path_detour_missing"], }, { "wire_uuid": "wire-ok", "wire_object_label": "N-OK: A3 -> B3 (Routed)", "issue_codes": ["collision_warnings"], }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_main_path_detour_missing_wires() self.assertEqual(2, result["selected_main_path_detour_missing_wires"]) self.assertEqual( ["QETRoutedConnection_sampled", "QETRoutedConnection_object_issue"], result["selected_main_path_detour_missing_wire_names"], ) self.assertEqual(["wire-missing"], result["missing_main_path_detour_missing_wire_refs"]) self.assertEqual([sampled_wire, object_issue_wire], selected) def test_controller_selects_issue_route_sources_from_latest_batch_diagnostic(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") source_path = doc.addObject("Part::Feature", "YellowMainRouteSketch") source_path.Label = "黄色主路径" carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], label="QET User Route Path 黄色主路径", project_uuid="project-1", kind="UserPath", ) carrier.QetRouteSourceName = "YellowMainRouteSketch" carrier.QetRouteSourceLabel = "黄色主路径" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "route_samples": [ { "wire_uuid": "wire-boundary", "wire_object_label": "N-BOUNDARY: A2 -> B2 (BoundaryWarning)", "issue_codes": ["boundary_warning"], "carrier_names": [carrier.Name], "route_track": { "segments": [ { "carrier": { "name": carrier.Name, "label": carrier.Label, "source_name": "YellowMainRouteSketch", "source_label": "黄色主路径", } } ] }, }, { "wire_uuid": "wire-ok", "wire_object_label": "N-OK: A3 -> B3 (Routed)", "issue_codes": [], "carrier_names": ["MissingCarrier"], }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_issue_route_sources() self.assertEqual(2, result["selected_issue_route_objects"]) self.assertEqual([carrier.Name], result["selected_issue_route_carrier_names"]) self.assertEqual(["YellowMainRouteSketch"], result["selected_issue_route_source_names"]) self.assertEqual([], result["missing_issue_route_refs"]) self.assertEqual([carrier, source_path], selected) def test_controller_selects_main_path_detour_missing_route_sources_from_latest_batch_diagnostic(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") source_path = doc.addObject("Part::Feature", "YellowMainPathSketch") source_path.Label = "黄色主路径" carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], label="QET User Route Path 黄色主路径", project_uuid="project-1", kind="UserPath", ) carrier.QetRouteSourceName = source_path.Name carrier.QetRouteSourceLabel = source_path.Label diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "route_samples": [ { "wire_uuid": "wire-main-path", "wire_object_label": "N-MAINPATH: A1 -> B1 (CollisionWarning)", "issue_codes": ["collision_warnings", "main_path_detour_missing"], "carrier_names": [carrier.Name], "route_track": { "segments": [ { "carrier": { "name": carrier.Name, "label": carrier.Label, "source_name": source_path.Name, "source_label": source_path.Label, } } ] }, }, { "wire_uuid": "wire-long", "wire_object_label": "N-LONG: A2 -> B2 (LongAccessWarning)", "issue_codes": ["long_terminal_access"], "carrier_names": ["MissingLongAccessCarrier"], }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_main_path_detour_missing_route_sources() self.assertEqual(2, result["selected_main_path_detour_route_objects"]) self.assertEqual([carrier.Name], result["selected_main_path_detour_route_carrier_names"]) self.assertEqual([source_path.Name], result["selected_main_path_detour_route_source_names"]) self.assertEqual([], result["missing_main_path_detour_route_refs"]) self.assertEqual([carrier, source_path], selected) def test_controller_selects_main_path_detour_route_sources_from_wire_object_track(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") source_path = doc.addObject("Part::Feature", "YellowMainPathSketch") source_path.Label = "黄色主路径" carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], label="QET User Route Path 黄色主路径", project_uuid="project-1", kind="UserPath", ) carrier.QetRouteSourceName = source_path.Name carrier.QetRouteSourceLabel = source_path.Label diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps({"route_samples": []}, ensure_ascii=False) diagnostic_group.addObject(diagnostic) routed_group = wiring_objects.ensure_routed_group(doc, "project-1") wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path") wire.Label = "N-MAINPATH: A1 -> B1 (CollisionWarning)" wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-main-path" wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteTrackJson"] wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" wire.QetRouteTrackJson = json.dumps( { "carrier_names": [carrier.Name], "segments": [ { "carrier": { "name": carrier.Name, "label": carrier.Label, "source_name": source_path.Name, "source_label": source_path.Label, } } ], }, ensure_ascii=False, ) routed_group.addObject(wire) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_main_path_detour_missing_route_sources() self.assertEqual(2, result["selected_main_path_detour_route_objects"]) self.assertEqual([carrier.Name], result["selected_main_path_detour_route_carrier_names"]) self.assertEqual([source_path.Name], result["selected_main_path_detour_route_source_names"]) self.assertEqual([], result["missing_main_path_detour_route_refs"]) self.assertEqual([carrier, source_path], selected) def test_controller_selects_route_sources_from_selected_wire_track(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") source_path = doc.addObject("Part::Feature", "YellowMainRouteSketch") source_path.Label = "黄色主路径" carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], label="QET User Route Path 黄色主路径", project_uuid="project-1", kind="UserPath", ) routed_wire = doc.addObject("Part::Feature", "QETRoutedConnection_problem") routed_wire.RouteType = "RoutedConnection" routed_wire.QetRouteTrackJson = json.dumps( { "carrier_names": [carrier.Name], "segments": [ { "carrier": { "name": carrier.Name, "label": carrier.Label, "source_name": source_path.Name, "source_label": source_path.Label, } } ], }, ensure_ascii=False, ) selected = [routed_wire] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_selected_wire_route_sources() self.assertEqual(2, result["selected_wire_route_objects"]) self.assertEqual([carrier.Name], result["selected_wire_route_carrier_names"]) self.assertEqual([source_path.Name], result["selected_wire_route_source_names"]) self.assertEqual([carrier, source_path], selected) def test_controller_selects_rejected_fallback_sources_from_selected_wire_diagnostics(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") fallback_source = doc.addObject("Part::Feature", "AuxiliaryRoutingRangeSource") fallback_source.Label = "辅助面" routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(120, 0, 20)], label="QET Routing Range 辅助面", project_uuid="project-1", kind="RoutingRange", ) routed_wire = doc.addObject("Part::Feature", "QETRoutedConnection_missing_main_path") routed_wire.RouteType = "RoutedConnection" routed_wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" routed_wire.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["RoutingRange"], "rejected_fallback_labels": ["辅助面", "缺失辅助路径"], } }, ensure_ascii=False, ) selected = [routed_wire] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_selected_wire_rejected_fallback_sources() self.assertEqual(1, result["selected_rejected_fallback_sources"]) self.assertEqual([fallback_source.Name], result["selected_rejected_fallback_source_names"]) self.assertEqual(["辅助面", "缺失辅助路径"], result["rejected_fallback_source_labels"]) self.assertEqual(["RoutingRange"], result["rejected_fallback_source_kinds"]) self.assertEqual(["QETRoutedConnection_missing_main_path"], result["selected_rejected_fallback_wire_names"]) self.assertEqual(["缺失辅助路径"], result["missing_rejected_fallback_source_refs"]) self.assertEqual([fallback_source], selected) def test_controller_selects_main_path_detour_rejected_fallback_sources_from_summary(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") wiring_objects.ensure_diagnostic_group(doc, "project-1") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") fallback_source.Label = "QET Routing Range Carrier 001" fallback_source.QetRouteSourceLabel = "安装板布线面" fallback_source.QetRouteSourceName = "CabinetBackPlateSketch" wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path") wire.Label = "N-MAINPATH: A1 -> B1" wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-main-path" wire.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", ] wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" wire.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["RoutingRange"], "rejected_fallback_labels": ["安装板布线面", "缺失补路位置"], } }, ensure_ascii=False, ) routed_group.addObject(wire) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_main_path_detour_rejected_fallback_sources() self.assertEqual(1, result["selected_main_path_detour_rejected_fallback_sources"]) self.assertEqual([fallback_source.Name], result["selected_main_path_detour_rejected_fallback_source_names"]) self.assertEqual(["安装板布线面", "缺失补路位置"], result["main_path_detour_rejected_fallback_labels"]) self.assertEqual( {"安装板布线面": 1, "缺失补路位置": 1}, result["main_path_detour_rejected_fallback_label_counts"], ) self.assertEqual({"RoutingRange": 1}, result["main_path_detour_rejected_fallback_kind_counts"]) self.assertEqual(["缺失补路位置"], result["missing_main_path_detour_rejected_fallback_refs"]) self.assertEqual([fallback_source], selected) def test_controller_selects_main_path_detour_bridge_endpoint_sources_from_summary(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") wiring_objects.ensure_diagnostic_group(doc, "project-1") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") fallback_source = doc.addObject("Part::Feature", "CabinetRoutingRangeSource") fallback_source.QetRouteSourceLabel = "安装板布线面" current_source = doc.addObject("Part::Feature", "MainWireDuctSource") current_source.QetRouteSourceLabel = "主线槽A" wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_pair") wire.Label = "N-PAIR: A1 -> B1" wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-main-path-pair" wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" wire.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", "QetRouteTrackJson", ] wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["RoutingRange"], "rejected_fallback_labels": ["安装板布线面"], } }, ensure_ascii=False, ) wire.QetRouteTrackJson = json.dumps( { "segments": [ { "carrier": { "kind": "WireDuct", "source_label": "主线槽A", "source_name": current_source.Name, } } ] }, ensure_ascii=False, ) routed_group.addObject(wire) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_main_path_detour_rejected_fallback_sources() self.assertEqual(2, result["selected_main_path_detour_bridge_endpoint_objects"]) self.assertEqual(1, result["selected_main_path_detour_rejected_fallback_sources"]) self.assertEqual(1, result["selected_main_path_detour_current_route_sources"]) self.assertEqual([fallback_source.Name], result["selected_main_path_detour_rejected_fallback_source_names"]) self.assertEqual([current_source.Name], result["selected_main_path_detour_current_route_source_names"]) self.assertEqual({"主线槽A": 1}, result["main_path_detour_current_route_source_label_counts"]) self.assertEqual({"安装板布线面 -> 主线槽A": 1}, result["main_path_detour_bridge_pair_counts"]) self.assertEqual([], result["missing_main_path_detour_current_route_refs"]) self.assertEqual([fallback_source, current_source], selected) def test_controller_selects_issue_wires_from_wire_object_issue_codes(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") sampled_wire = doc.addObject("Part::Feature", "QETRoutedConnection_sampled") sampled_wire.Label = "N-SAMPLED: A1 -> B1 (BoundaryWarning)" sampled_wire.RouteType = "RoutedConnection" sampled_wire.QetWireUuid = "wire-sampled" hidden_issue_wire = doc.addObject("Part::Feature", "QETRoutedConnection_hidden") hidden_issue_wire.Label = "N-HIDDEN: A2 -> B2 (LongAccessWarning)" hidden_issue_wire.RouteType = "RoutedConnection" hidden_issue_wire.QetWireUuid = "wire-hidden" hidden_issue_wire.QetRouteIssueCodes = "long_terminal_access" normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_normal") normal_wire.Label = "N-OK: A3 -> B3 (Routed)" normal_wire.RouteType = "RoutedConnection" normal_wire.QetWireUuid = "wire-ok" normal_wire.QetRouteIssueCodes = "" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "route_samples": [ { "wire_uuid": "wire-sampled", "wire_object_label": "N-SAMPLED: A1 -> B1 (BoundaryWarning)", "issue_codes": ["boundary_warning"], } ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_issue_wires() self.assertEqual(2, result["selected_issue_wires"]) self.assertEqual( ["QETRoutedConnection_sampled", "QETRoutedConnection_hidden"], result["selected_issue_wire_names"], ) self.assertEqual([], result["missing_issue_wire_refs"]) self.assertEqual([sampled_wire, hidden_issue_wire], selected) def test_controller_selects_long_terminal_accesses_from_latest_batch_diagnostic(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") terminal_a = _terminal(doc, terminal_objects, "Terminal325", "terminal-325", app.Vector(0, 0, 0)) terminal_b = _terminal(doc, terminal_objects, "Terminal326", "terminal-326", app.Vector(10, 0, 0)) diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "routing_path_network_diagnostic": { "long_terminal_accesses": [ {"terminal_uuid": "terminal-325", "name": "Terminal325", "label": "325"}, {"terminal_uuid": "terminal-326", "name": "Terminal326", "label": "326"}, {"terminal_uuid": "terminal-404", "name": "Terminal404", "label": "404"}, ] } }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_long_terminal_accesses() self.assertEqual(2, result["selected_long_terminal_accesses"]) self.assertEqual(["Terminal325", "Terminal326"], result["selected_long_terminal_names"]) self.assertEqual(["terminal-404"], result["missing_long_terminal_refs"]) self.assertEqual([terminal_a, terminal_b], selected) def test_controller_selects_long_terminal_access_devices_from_latest_batch_diagnostic(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") device_pen = doc.addObject("App::DocumentObjectGroup", "DevicePEN") device_pen.Label = "PEN" device_pe = doc.addObject("App::DocumentObjectGroup", "DevicePE") device_pe.Label = "PE" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "routing_path_network_diagnostic": { "long_terminal_accesses": [ { "terminal_uuid": "terminal-325", "parent_device_name": "DevicePEN", "parent_device_label": "PEN", }, { "terminal_uuid": "terminal-326", "parent_device_name": "DevicePEN", "parent_device_label": "PEN", }, { "terminal_uuid": "terminal-327", "parent_device_label": "PE", }, { "terminal_uuid": "terminal-404", "parent_device_name": "Device404", "parent_device_label": "404", }, ] } }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_long_terminal_access_devices() self.assertEqual(2, result["selected_long_terminal_access_devices"]) self.assertEqual(["DevicePEN", "DevicePE"], result["selected_long_terminal_access_device_names"]) self.assertEqual(["Device404"], result["missing_long_terminal_access_device_refs"]) self.assertEqual([device_pen, device_pe], selected) def test_controller_selects_missing_terminal_devices_from_latest_batch_diagnostic(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") device_a = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") terminal_objects.ensure_string_property(device_a, "QetElementUuid", "QET Exchange", "", "device-a") terminal_objects.ensure_string_property(device_a, "QetInstanceId", "QET Exchange", "", "instance-a") device_b = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_b") terminal_objects.ensure_string_property(device_b, "QetElementUuid", "QET Exchange", "", "device-b") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-missing-a", "start_instance_id": "instance-a", "start_element_uuid": "device-a", "end_found": True, }, { "wire_uuid": "wire-b", "start_found": False, "start_terminal_uuid": "terminal-missing-b", "start_instance_id": "instance-missing", "start_element_uuid": "device-missing", "end_found": False, "end_terminal_uuid": "terminal-missing-c", "end_element_uuid": "device-b", }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() self.assertEqual(2, result["selected_missing_terminal_devices"]) self.assertEqual(["QETDevice_device_a", "QETDevice_device_b"], result["selected_missing_terminal_device_names"]) self.assertEqual(["terminal-missing-b"], result["missing_terminal_device_refs"]) self.assertEqual([device_a, device_b], selected) def test_controller_selects_missing_terminal_device_by_device_label_fallback(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") device = doc.addObject("App::DocumentObjectGroup", "QETDevice_no_uuid") device.Label = "缺端子设备A" diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-missing-a", "start_device_label": "缺端子设备A", "end_found": True, } ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() self.assertEqual(1, result["selected_missing_terminal_devices"]) self.assertEqual(["QETDevice_no_uuid"], result["selected_missing_terminal_device_names"]) self.assertEqual([], result["missing_terminal_device_refs"]) self.assertEqual([device], selected) def test_controller_reports_missing_terminal_device_reason_counts_when_device_not_in_scene(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") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-missing-a", "start_element_uuid": "device-a", "start_instance_id": "instance-a", "start_device_label": "UD:8", "start_terminal_display": "as", "start_missing_endpoint_reason_code": "device_not_in_3d_scene", "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", "end_found": True, } ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_missing_terminal_devices() self.assertEqual(0, result["selected_missing_terminal_devices"]) self.assertEqual(["terminal-missing-a"], result["missing_terminal_device_refs"]) self.assertEqual(["UD:8"], result["missing_terminal_device_labels"]) self.assertEqual(["instance-a"], result["missing_terminal_device_instance_ids"]) self.assertEqual(["device-a"], result["missing_terminal_device_element_uuids"]) self.assertEqual({"device_not_in_3d_scene": 1}, result["missing_terminal_device_reason_counts"]) self.assertEqual([], selected) def test_controller_selects_found_counterpart_terminals_from_missing_endpoint_samples(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") end_terminal = _terminal(doc, terminal_objects, "TerminalFoundEnd", "terminal-found-end", app.Vector(20, 0, 0)) start_terminal = _terminal( doc, terminal_objects, "TerminalFoundStart", "terminal-found-start", app.Vector(40, 0, 0), ) diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "wire_label": "F6", "start_found": False, "start_terminal_uuid": "terminal-missing-start", "end_found": True, "end_terminal_uuid": "terminal-found-end", "end_terminal_display": "6", }, { "wire_uuid": "wire-b", "wire_label": "N2", "start_found": True, "start_terminal_uuid": "terminal-found-start", "start_terminal_display": "1", "end_found": False, "end_terminal_uuid": "terminal-missing-end", }, { "wire_uuid": "wire-c", "start_found": False, "start_terminal_uuid": "terminal-missing-both-a", "end_found": False, "end_terminal_uuid": "terminal-missing-both-b", }, ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_missing_terminal_counterpart_terminals() self.assertEqual(2, result["selected_missing_terminal_counterpart_terminals"]) self.assertEqual(["TerminalFoundEnd", "TerminalFoundStart"], result["selected_missing_terminal_counterpart_terminal_names"]) self.assertEqual(["terminal-missing-both-a", "terminal-missing-both-b"], result["missing_terminal_counterpart_refs"]) self.assertEqual([end_terminal, start_terminal], selected) def test_controller_selects_missing_terminal_candidate_terminals_from_latest_batch_diagnostic(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") candidate_a1 = _terminal(doc, terminal_objects, "TerminalA1", "terminal-a1", app.Vector(0, 0, 0)) candidate_a2 = _terminal(doc, terminal_objects, "TerminalA2", "terminal-a2", app.Vector(10, 0, 0)) found_b1 = _terminal(doc, terminal_objects, "TerminalB1", "terminal-b1", app.Vector(20, 0, 0)) for terminal in (candidate_a1, candidate_a2): terminal_objects.ensure_string_property(terminal, "QetElementUuid", "QET Exchange", "", "device-a") terminal_objects.ensure_string_property(terminal, "QetInstanceId", "QET Exchange", "", "instance-a") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-missing-a", "start_instance_id": "instance-a", "start_element_uuid": "device-a", "start_missing_endpoint_reason_code": "terminal_uuid_not_in_element", "start_instance_terminal_samples": [ {"name": "TerminalA1", "terminal_uuid": "terminal-a1"}, {"name": "TerminalA2", "terminal_uuid": "terminal-a2"}, ], "end_found": True, "end_terminal_uuid": "terminal-b1", } ] }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_missing_terminal_candidate_terminals() self.assertEqual(2, result["selected_missing_terminal_candidate_terminals"]) self.assertEqual(["TerminalA1", "TerminalA2"], result["selected_missing_terminal_candidate_terminal_names"]) self.assertEqual([], result["missing_terminal_candidate_terminal_refs"]) self.assertEqual([candidate_a1, candidate_a2], selected) self.assertNotIn(found_b1, selected) def test_controller_selects_boundary_issue_route_carriers_and_terminals(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 = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(140, 0, 20)], label="柜内主路径A", project_uuid="project-1", kind="UserPath", ) terminal = _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPathNetwork") diagnostic.QetDiagnosticKind = "RoutingPathNetwork" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticJson = json.dumps( { "route_carriers_outside_boundary": [ { "carrier": { "name": route.Name, "label": "柜内主路径A", }, "outside_point_count": 1, }, { "carrier": { "name": "MissingRouteCarrier", "label": "缺失路径", }, "outside_point_count": 1, }, ], "terminals_outside_boundary": [ { "name": "TerminalOutside", "label": "TerminalOutside", "terminal_uuid": "terminal-outside", "outside_point_count": 2, }, { "name": "MissingTerminal", "terminal_uuid": "terminal-missing", "outside_point_count": 1, }, ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) selected = [] gui.Selection = types.SimpleNamespace( clearSelection=lambda: selected.clear(), addSelection=lambda obj: selected.append(obj), getSelection=lambda: list(selected), getSelectionEx=lambda: [], ) result = auto_routing_panel.AutoRoutingController().select_boundary_issue_route_carriers_and_terminals() self.assertEqual(1, result["selected_boundary_route_carriers"]) self.assertEqual(1, result["selected_boundary_terminals"]) self.assertEqual([route.Name], result["selected_boundary_route_carrier_names"]) self.assertEqual(["TerminalOutside"], result["selected_boundary_terminal_names"]) self.assertEqual(["MissingRouteCarrier"], result["missing_boundary_route_carrier_refs"]) self.assertEqual(["MissingTerminal"], result["missing_boundary_terminal_refs"]) self.assertEqual([route, terminal], selected) def test_controller_marks_selected_route_carrier_constraint_modes(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") carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="测试路径", project_uuid="project-1", kind="UserPath", ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=carrier)], ) controller = auto_routing_panel.AutoRoutingController() forbidden = controller.mark_selected_route_carriers_forbidden() required = controller.mark_selected_route_carriers_required() cleared = controller.clear_selected_route_carrier_constraints() self.assertEqual(1, forbidden["route_constraint_carriers"]) self.assertEqual(1, required["route_constraint_carriers"]) self.assertEqual(1, cleared["route_constraint_carriers"]) self.assertEqual("", carrier.QetRouteConstraintMode) def test_controller_sets_selected_route_carrier_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") source = doc.addObject("Sketcher::SketchObject", "CapacityPathSketch") source.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="测试路径", project_uuid="project-1", kind="UserPath", ) routing_network._mark_user_path_source(source, carrier) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [FakeSelectionItem(obj=source)], ) controller = auto_routing_panel.AutoRoutingController(options={"selected_route_capacity": 5}) report = controller.set_selected_route_carriers_capacity() self.assertEqual(1, report["route_capacity_carriers"]) self.assertEqual(1, report["route_capacity_sources"]) self.assertEqual(5, source.QetRouteCarrierCapacity) self.assertEqual(5, carrier.QetRouteCarrierCapacity) def test_controller_reports_selected_source_route_constraint_before_carrier_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("Sketcher::SketchObject", "FutureUserRouteSketch") 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)], ) report = auto_routing_panel.AutoRoutingController().mark_selected_route_carriers_required() self.assertEqual(0, report["route_constraint_carriers"]) self.assertEqual(1, report["route_constraint_sources"]) self.assertEqual("Required", route_path.QetRouteConstraintMode) def test_controller_clears_all_route_carrier_constraint_modes(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") required = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="必经路径", project_uuid="project-1", kind="UserPath", ) forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="禁经路径", project_uuid="project-1", kind="UserPath", ) required.QetRouteConstraintMode = "Required" forbidden.QetRouteConstraintMode = "Forbidden" controller = auto_routing_panel.AutoRoutingController() report = controller.clear_all_route_carrier_constraints() self.assertEqual(2, report["route_constraint_carriers"]) self.assertEqual("", required.QetRouteConstraintMode) self.assertEqual("", forbidden.QetRouteConstraintMode) self.assertNotIn("路径约束", controller.summary()) def test_selected_source_route_constraint_survives_carrier_regeneration(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") 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))], ) selection = [FakeSelectionItem(obj=route_path)] first = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") routing_network.clear_route_carriers(doc) second = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) self.assertEqual(1, len(first)) self.assertEqual(1, len(second)) self.assertEqual("Required", route_path.QetRouteConstraintMode) self.assertEqual("Required", second[0].QetRouteConstraintMode) def test_refreshing_user_path_clears_stale_constraint_when_source_is_cleared(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") 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))], ) selection = [FakeSelectionItem(obj=route_path)] carriers = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) route_path.QetRouteConstraintMode = "" carriers[0].QetRouteConstraintMode = "Required" refreshed = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) self.assertEqual(1, len(refreshed)) self.assertEqual("", refreshed[0].QetRouteConstraintMode) def test_selected_multi_wire_source_route_constraint_marks_all_user_paths(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") route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), ], ) selection = [FakeSelectionItem(obj=route_path)] carriers = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) marked = routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") self.assertEqual(2, len(carriers)) self.assertEqual(2, len(marked)) self.assertEqual("Required", route_path.QetRouteConstraintMode) self.assertEqual(["Required", "Required"], [carrier.QetRouteConstraintMode for carrier in carriers]) def test_controller_clears_selected_multi_wire_source_route_constraints(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("Sketcher::SketchObject", "MultiWireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), ], ) selection = [FakeSelectionItem(obj=route_path)] carriers = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: selection, ) cleared = auto_routing_panel.AutoRoutingController().clear_selected_route_carrier_constraints() self.assertEqual(2, cleared["route_constraint_carriers"]) self.assertEqual("", route_path.QetRouteConstraintMode) self.assertEqual(["", ""], [carrier.QetRouteConstraintMode for carrier in carriers]) def test_clear_all_route_constraints_clears_source_objects_too(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") 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))], ) selection = [FakeSelectionItem(obj=route_path)] carriers = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") report = routing_network.clear_all_route_constraint_modes(doc) self.assertEqual(1, report["route_constraint_carriers"]) self.assertEqual(1, report["route_constraint_sources"]) self.assertEqual("", route_path.QetRouteConstraintMode) self.assertEqual("", carriers[0].QetRouteConstraintMode) 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_selected_multi_wire_user_path_copies_source_capacity_to_all_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("Sketcher::SketchObject", "CapacityMultiWireRouteSketch") route_path.QetRouteCarrierCapacity = 4 route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 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(2, result["user_path_carriers"]) self.assertEqual([4, 4], [carrier.QetRouteCarrierCapacity for carrier in carriers]) def test_selected_user_path_projects_line_to_selected_face(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") face = FakeFace( FakeBoundBox(0, 100, 0, 100, 0, 0), app.Vector(0, 0, 1), ) draft_line = doc.addObject("Part::Feature", "FloatingDraftLine") draft_line.Shape = FakeShape( FakeBoundBox(10, 90, 10, 90, 25, 35), edges=[FakeEdge(app.Vector(10, 10, 25), app.Vector(90, 90, 35))], ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [ FakeSelectionItem([face]), FakeSelectionItem(obj=draft_line), ], ) 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([2.0, 2.0], [point.z for point in carriers[0].Points]) 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_terminal_access_accepts_object_wrapped_local_route_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") terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) terminal.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") terminal.QetTerminalLocalRoutePointsJson = json.dumps( { "points": [ {"x": 0, "y": 0, "z": 0}, {"x": 10, "y": 0, "z": 0}, {"x": 10, "y": 30, "z": 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_controller_sets_selected_terminal_local_route_from_selected_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") terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(100, 10, 0)) route_path = doc.addObject("Part::Feature", "LocalExitSketch") route_path.Shape = FakeShape( FakeBoundBox(100, 130, 10, 40, 0, 0), edges=[ FakeEdge(app.Vector(100, 10, 0), app.Vector(130, 10, 0)), FakeEdge(app.Vector(130, 10, 0), app.Vector(130, 40, 0)), ], ) gui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=lambda: [ FakeSelectionItem(obj=terminal), FakeSelectionItem(obj=route_path), ], ) result = auto_routing_panel.AutoRoutingController().set_selected_terminal_local_route_points() points = json.loads(terminal.QetTerminalLocalRoutePointsJson) access_points = routing_network.terminal_access_path_points(terminal, exit_length=20.0) self.assertEqual(1, result["terminal_local_routes"]) self.assertEqual("TerminalStart", result["terminal_local_route_names"][0]) self.assertEqual( [[0.0, 0.0, 0.0], [30.0, 0.0, 0.0], [30.0, 30.0, 0.0]], points, ) self.assertEqual( [(100.0, 10.0, 0.0), (130.0, 10.0, 0.0), (130.0, 40.0, 0.0)], [(point.x, point.y, point.z) for point in access_points], ) 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(1, 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(50, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], project_uuid="project-1", kind="RoutingRange", label="近处布线面", ) routing_network.create_route_carrier( doc, [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", kind="WireDuct", label="较远线槽", ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=1000.0, ) self.assertEqual(1, len(created)) end_point = created[0].Points[-1] self.assertEqual((50.0, 100.0, 20.0), (end_point.x, end_point.y, end_point.z)) def test_terminal_access_prefers_main_path_over_routing_range_in_same_component(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(50, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 5, 20), app.Vector(100, 5, 20)], project_uuid="project-1", kind="RoutingRange", label="近处布线面", ) routing_network.create_route_carrier( doc, [app.Vector(0, 100, 20), app.Vector(100, 100, 20)], project_uuid="project-1", kind="WireDuct", label="较远线槽", ) routing_network.create_route_carrier( doc, [app.Vector(50, 5, 20), app.Vector(50, 100, 20)], project_uuid="project-1", kind="UserPath", label="线槽接入桥", ) network = routing_network.build_route_graph(doc) ranked = routing_network.rank_connection_point_candidates( network, routing_network.connection_point_candidates(network, app.Vector(50, 0, 20), limit=0), ) created = routing_network.create_terminal_access_carriers_from_document( doc, project_uuid="project-1", terminal_exit_length=20.0, max_distance=1000.0, ) self.assertEqual(1, len(created)) self.assertEqual("UserPath", getattr(ranked[0]["carrier"], "QetRouteCarrierKind", "")) end_point = created[0].Points[-1] self.assertEqual((50.0, 5.0, 20.0), (end_point.x, end_point.y, end_point.z)) def test_diverse_connection_entry_candidates_keep_multiple_components(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") near = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="近组件", ) far = routing_network.create_route_carrier( doc, [app.Vector(0, 100, 0), app.Vector(100, 100, 0)], project_uuid="project-1", kind="RoutingRange", label="远组件", ) network = routing_network.build_route_graph(doc) near_key = routing_network._point_key(app.Vector(0, 0, 0)) far_key = routing_network._point_key(app.Vector(0, 100, 0)) candidates = [ { "key": near_key, "projected_key": routing_network._point_key(app.Vector(index, 0, 0)), "point": app.Vector(index, 0, 0), "distance": index, "carrier": near, } for index in range(1, 6) ] candidates.append( { "key": far_key, "projected_key": far_key, "point": app.Vector(0, 100, 0), "distance": 100.0, "carrier": far, } ) selected = routing_network.select_diverse_connection_point_candidates(network, candidates, limit=3) self.assertEqual(3, len(selected)) self.assertIn(far, [candidate.get("carrier") for candidate in selected]) 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.assertIn("unconnected_terminals", result["issue_codes"]) self.assertEqual("RoutingPathNetwork", diagnostic_group.Group[0].QetDiagnosticKind) self.assertEqual("project-1", diagnostic_group.Group[0].QetProjectUuid) self.assertFalse(diagnostic_group.Group[0].QetDiagnosticOk) self.assertIn("unconnected_terminals", diagnostic_group.Group[0].QetDiagnosticIssueCodes) self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticIssueLabels) self.assertIn("端子未接入", diagnostic_group.Group[0].QetDiagnosticMessage) self.assertIn("unconnected_terminals", payload["issue_codes"]) 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") device = doc.addObject("App::Part", "DevicePEN") device.Label = "PEN" device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) terminal = _terminal(doc, terminal_objects, "TerminalLongAccess", "terminal-long-access", app.Vector(0, 0, 0)) device.addObject(terminal) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 20), app.Vector(1100, 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"])) long_access = payload["long_terminal_accesses"][0] self.assertEqual("terminal-long-access", long_access["terminal_uuid"]) self.assertEqual(900.0, long_access["terminal_access_length_mm"]) self.assertEqual("PEN", long_access["parent_device_label"]) self.assertEqual("DevicePEN", long_access["parent_device_name"]) self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, long_access["terminal_origin"]) self.assertEqual("x", long_access["terminal_access_dominant_axis"]) self.assertEqual(2, len(long_access["terminal_access_points"])) self.assertEqual({"x": 100.0, "y": 0.0, "z": 20.0}, long_access["terminal_access_points"][0]) self.assertEqual({"x": 1000.0, "y": 0.0, "z": 20.0}, long_access["terminal_access_points"][1]) 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_ignores_isolated_routing_range_only_components(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, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(0, 40, 0)], project_uuid="project-1", kind="TerminalAccess", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], project_uuid="project-1", kind="RoutingRange", label="孤立布线面", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") self.assertNotIn("isolated_network_components", result["issue_codes"]) self.assertEqual(0, len(result["diagnostic"]["isolated_components"])) def test_check_routing_path_network_warns_for_isolated_primary_route_components(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, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(0, 40, 0)], project_uuid="project-1", kind="TerminalAccess", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1100, 0, 0)], project_uuid="project-1", kind="UserPath", label="孤立用户路径", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") self.assertIn("isolated_network_components", result["issue_codes"]) self.assertEqual(2, len(result["diagnostic"]["isolated_components"])) def test_check_routing_path_network_warns_for_wire_duct_without_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, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="孤立线槽", ) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 0), app.Vector(1000, 100, 0)], project_uuid="project-1", kind="TerminalAccess", label="端子接入", ) 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.assertIn("wire_ducts_without_terminal_access", result["issue_codes"]) self.assertEqual(1, len(result["diagnostic"]["wire_ducts_without_terminal_access"])) suggestion = result["diagnostic"]["wire_ducts_without_terminal_access"][0]["bridge_suggestion"] self.assertEqual("孤立线槽", suggestion["from_carrier"]["label"]) self.assertEqual("端子接入", suggestion["to_carrier"]["label"]) self.assertEqual(900.0, suggestion["distance_mm"]) self.assertEqual({"x": 100.0, "y": 0.0, "z": 0.0}, suggestion["from_point"]) self.assertEqual({"x": 1000.0, "y": 0.0, "z": 0.0}, suggestion["to_point"]) compact_suggestion = payload["wire_ducts_without_terminal_access"][0]["bridge_suggestion"] self.assertEqual("端子接入", compact_suggestion["to_carrier"]["label"]) self.assertIn("线槽未接入端子主网络", message) self.assertIn("建议桥接到 端子接入", message) self.assertIn("900.0 mm", message) def test_zero_distance_user_path_endpoint_splits_wire_duct_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") routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 100, 0)], project_uuid="project-1", kind="WireDuct", label="斜向线槽", ) routing_network.create_route_carrier( doc, [app.Vector(50, 50, 0), app.Vector(50, 90, 0)], project_uuid="project-1", kind="UserPath", label="零距离桥接", ) routing_network.create_route_carrier( doc, [app.Vector(50, 90, 0), app.Vector(50, 130, 0)], project_uuid="project-1", kind="TerminalAccess", label="端子接入", ) result = auto_routing.check_eplan_routing_path_network(doc, project_uuid="project-1") self.assertNotIn("wire_ducts_without_terminal_access", result["issue_codes"]) def test_create_user_path_bridge_from_selection_connects_nearest_route_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") duct = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="WireDuct", label="线槽", ) main_path = routing_network.create_route_carrier( doc, [app.Vector(120, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", kind="RoutingRange", label="主网络", ) created = routing_network.create_user_path_bridge_from_selection( doc, [ types.SimpleNamespace(Object=duct), types.SimpleNamespace(Object=main_path), ], project_uuid="project-1", ) self.assertEqual(1, len(created)) self.assertEqual("UserPath", created[0].QetRouteCarrierKind) self.assertEqual([(100.0, 0.0, 0.0), (120.0, 20.0, 0.0)], [ (point.x, point.y, point.z) for point in created[0].Points ]) def test_create_user_path_bridge_between_source_objects_uses_nearest_carrier_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") fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") fallback_source.Label = "门板布线面" current_source = doc.addObject("Part::Feature", "MainDuctSource") current_source.Label = "主线槽" far_fallback = routing_network.create_route_carrier( doc, [app.Vector(-500, 0, 0), app.Vector(-400, 0, 0)], project_uuid="project-1", kind="RoutingRange", label="远处布线面", ) near_fallback = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(100, 0, 0)], project_uuid="project-1", kind="RoutingRange", label="近处布线面", ) main_path = routing_network.create_route_carrier( doc, [app.Vector(130, 20, 0), app.Vector(200, 20, 0)], project_uuid="project-1", kind="WireDuct", label="主线槽路径", ) for carrier in (far_fallback, near_fallback): carrier.QetRouteSourceName = fallback_source.Name carrier.QetRouteSourceLabel = fallback_source.Label main_path.QetRouteSourceName = current_source.Name main_path.QetRouteSourceLabel = current_source.Label created = routing_network.create_user_path_bridge_between_objects( doc, fallback_source, current_source, project_uuid="project-1", ) self.assertEqual(1, len(created)) self.assertEqual("UserPath", created[0].QetRouteCarrierKind) self.assertEqual( [(100.0, 0.0, 0.0), (130.0, 20.0, 0.0)], [(point.x, point.y, point.z) for point in created[0].Points], ) self.assertEqual("MainPathDetourBridge", created[0].QetRouteBridgeKind) self.assertEqual("门板布线面 -> 主线槽", created[0].QetRouteBridgePairLabel) self.assertEqual(fallback_source.Name, created[0].QetRouteBridgeLeftSourceName) self.assertEqual(current_source.Name, created[0].QetRouteBridgeRightSourceName) duplicated = routing_network.create_user_path_bridge_between_objects( doc, fallback_source, current_source, project_uuid="project-1", ) self.assertEqual([], duplicated) 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_routing_path_network_diagnostic_message_tolerates_malformed_bridge_count(self): _install_fake_freecad() _terminal_objects, _wiring_objects, routing_network, _auto_routing = _reload_modules() diagnostic = { "summary": { "carriers": 1, "segments": 1, "nodes": 2, "bridged_segments": "not-a-number", }, "issues": [], } message = routing_network._routing_path_network_diagnostic_message(diagnostic) self.assertIn("布线路径网络检查通过", 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_access_warning_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, "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, ) controller = auto_routing_panel.AutoRoutingController() controller.set_terminal_access_max_distance(1000.0) controller.set_terminal_access_warning_distance(950.0) result = controller.check_routing_path_network() self.assertNotIn("long_terminal_accesses", result["issue_codes"]) self.assertEqual(950.0, controller.routing_options()["terminal_access_warning_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_auto_routing_controller_readiness_writes_preflight_diagnostic(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)) app._qet_exchange_payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", } ], } report = auto_routing_panel.AutoRoutingController().check_routing_readiness() diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") self.assertIn("missing_endpoints", report["issue_codes"]) self.assertIsNotNone(diagnostic_group) self.assertEqual(1, len(diagnostic_group.Group)) self.assertEqual("RoutingPreflight", diagnostic_group.Group[0].QetDiagnosticKind) 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_summary_includes_runtime_version(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") summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), summary) def test_auto_routing_controller_summary_includes_cabinet_boundary_count(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") boundary = doc.addObject("Part::Feature", "CabinetInteriorSpace") boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) boundary.QetRoutingBoundaryKind = "CabinetInterior" summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("柜内边界:1", summary) def test_auto_routing_controller_summary_includes_wire_style_database_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"] doc = FakeDocument() app.ActiveDocument = doc app._qet_exchange_summary = { "wire_style_database_path": "D:/project/project-local.sqlite", } terminal_objects.ensure_root_group(doc, "project-1") summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("导线样式库:D:/project/project-local.sqlite", summary) def test_auto_routing_controller_summary_reads_wire_style_database_path_from_payload(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 app._qet_exchange_payload = { "project_uuid": "project-1", "wire_style_database_path": "D:/project/payload-style.sqlite", "wires": [], } terminal_objects.ensure_root_group(doc, "project-1") summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("导线样式库:D:/project/payload-style.sqlite", summary) def test_auto_routing_controller_summary_prefers_current_payload_style_database_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"] doc = FakeDocument() app.ActiveDocument = doc app._qet_exchange_summary = { "project_uuid": "project-old", "wire_style_database_path": "D:/old/project-local.sqlite", } app._qet_exchange_payload = { "project_uuid": "project-current", "wire_style_database_path": "D:/current/project-local.sqlite", "wires": [], } terminal_objects.ensure_root_group(doc, "project-current") summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("导线样式库:D:/current/project-local.sqlite", summary) self.assertNotIn("D:/old/project-local.sqlite", summary) def test_auto_routing_controller_summary_includes_route_constraint_counts(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") required = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="UserPath", ) required.QetRouteConstraintMode = "Required" forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], project_uuid="project-1", kind="UserPath", ) forbidden.QetRouteConstraintMode = "Forbidden" summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("路径约束:必经 1,禁经 1", summary) def test_auto_routing_controller_summary_includes_source_route_constraint_counts(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") route_path = doc.addObject("Sketcher::SketchObject", "FutureUserRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), edges=[FakeEdge(app.Vector(0, 0, 20), app.Vector(100, 0, 20))], ) route_path.QetRouteConstraintMode = "Required" summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("源路径约束:必经 1,禁经 0", summary) def test_auto_routing_controller_summary_counts_wire_duct_source_route_constraints(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") wire_duct_source = doc.addObject("Part::Feature", "WireDuctBody") wire_duct_source.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) wire_duct_source.Shape.Solids = [object()] wire_duct_source.QetRoutingSourceKind = "WireDuct" wire_duct_source.QetRouteConstraintMode = "Forbidden" summary = auto_routing_panel.AutoRoutingController().summary() self.assertIn("源路径约束:必经 0,禁经 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_controller_exposes_obstacle_clearance(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", ) obstacle = doc.addObject("Part::Feature", "NearObstacle") obstacle.Shape = FakeShape(FakeBoundBox(40, 60, 3, 6, 15, 25)) controller = auto_routing_panel.AutoRoutingController() controller.set_obstacle_clearance(5.0) result = _auto_routing.route_eplan_connection_between_terminals( doc, start, end, options=controller.routing_options(), ) self.assertEqual(5.0, controller.routing_options()["obstacle_clearance"]) self.assertEqual("CollisionWarning", result["route_status"]) self.assertEqual("ClearanceWarning", result["collisions"][0]["collision_kind"]) self.assertEqual(["QET Route Carrier"], result["collisions"][0]["route_source_labels"]) diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) self.assertEqual(["QET Route Carrier"], diagnostics["collisions"][0]["route_source_labels"]) def test_auto_routing_controller_exposes_preflight_routeability_sample_limit(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, _auto_routing = _reload_modules() auto_routing_panel = importlib.import_module("AutoRoutingPanel") controller = auto_routing_panel.AutoRoutingController() controller.set_preflight_routeability_sample_limit(75) self.assertEqual(75, controller.routing_options()["preflight_routeability_sample_limit"]) def test_auto_routing_controller_exposes_segment_reuse_penalty(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)], 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", ) 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_segment_reuse_penalty(0.0) report = controller.route_eplan_connections() second_labels = [ segment["carrier"]["label"] for segment in report["routes"][1]["route_track"]["segments"] ] self.assertEqual(0.0, controller.routing_options()["segment_reuse_penalty"]) self.assertIn("Direct Duct", second_labels) self.assertNotIn("Alternate Duct", second_labels) 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_splits_disconnected_shape_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") route_path = doc.addObject("Sketcher::SketchObject", "MultiWireRouteSketch") route_path.Shape = FakeShape( FakeBoundBox(0, 120, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(40, 0, 20)]), FakeWire([app.Vector(80, 80, 20), app.Vector(120, 80, 20)]), ], ) created = routing_network.create_carriers_from_selection( doc, [FakeSelectionItem(obj=route_path)], project_uuid="project-1", ) self.assertEqual(2, len(created)) self.assertEqual( [ [(0.0, 0.0, 20.0), (40.0, 0.0, 20.0)], [(80.0, 80.0, 20.0), (120.0, 80.0, 20.0)], ], [[(point.x, point.y, point.z) for point in carrier.Points] for carrier in 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_keeps_clear_access_candidates_beyond_distance_limit(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 index in range(9): routing_network.create_route_carrier( doc, [app.Vector(20, index, 0), app.Vector(100, index, 0)], label="Near Blocked Duct {0}".format(index + 1), 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, 20, -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.assertTrue(all(not label.startswith("Near Blocked Duct") for label in labels)) self.assertEqual(0, result["network"]["route_candidate_obstacle_hits"]) def test_eplan_connection_route_prefers_carrier_inside_cabinet_boundary(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, 49, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 51, 0), app.Vector(100, 51, 0)], label="Outside Cabinet Path", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, -40, 0), app.Vector(100, -40, 0)], label="Inside Cabinet Path", project_uuid="project-1", kind="UserPath", ) boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) boundary.QetRoutingBoundaryKind = "CabinetInterior" 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("Inside Cabinet Path", labels) self.assertNotIn("Outside Cabinet Path", labels) self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) def test_eplan_connection_route_prefers_inside_detour_over_shorter_outside_shortcut(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, 0), app.Vector(100, 51, 0), app.Vector(100, 0, 0)], label="Outside Shortcut", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 0), app.Vector(0, -40, 0), app.Vector(100, -40, 0), app.Vector(100, 0, 0)], label="Inside Cabinet Detour", project_uuid="project-1", kind="UserPath", ) boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) boundary.QetRoutingBoundaryKind = "CabinetInterior" 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("Inside Cabinet Detour", labels) self.assertNotIn("Outside Shortcut", labels) self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) def test_eplan_connection_wire_records_boundary_warning_when_route_leaves_cabinet(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, 49, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 60, 0), app.Vector(100, 60, 0)], label="Only Outside Cabinet Path", project_uuid="project-1", kind="UserPath", ) boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) boundary.QetRoutingBoundaryKind = "CabinetInterior" result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) self.assertGreater(result["network"]["route_candidate_boundary_violations"], 0) self.assertTrue(result["wire"].QetRouteBoundaryAware) self.assertEqual("BoundaryWarning", result["wire"].QetRouteBoundaryStatus) self.assertEqual( str(result["network"]["route_candidate_boundary_violations"]), result["wire"].QetRouteBoundaryViolationCount, ) def test_eplan_connection_wire_records_long_network_access_warning(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, 125, 0), app.Vector(100, 125, 0)], label="Far Cabinet Main Path", project_uuid="project-1", kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "terminal_exit_length": 0.0, "lane_spacing": 0.0, "terminal_access_warning_distance": 50.0, }, ) wire = result["wire"] self.assertEqual("125.000", wire.QetRouteEntryDistanceMm) self.assertEqual("125.000", wire.QetRouteExitDistanceMm) self.assertEqual("node", wire.QetRouteEntryPointMode) self.assertEqual("node", wire.QetRouteExitPointMode) self.assertEqual("1", wire.QetRouteEntryCandidateRank) self.assertEqual("1", wire.QetRouteExitCandidateRank) self.assertEqual("50.000", wire.QetRouteAccessWarningDistanceMm) self.assertEqual("LongAccessWarning", wire.QetRouteAccessStatus) self.assertEqual("entry,exit", wire.QetRouteAccessWarningSides) payload = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual("LongAccessWarning", payload["access"]["access_status"]) self.assertEqual(["entry", "exit"], payload["access"]["warning_sides"]) def test_eplan_connection_route_keeps_inside_boundary_candidates_beyond_distance_limit(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, 49, 0)) end = _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 49, 0)) for index in range(9): y = 51 + index routing_network.create_route_carrier( doc, [app.Vector(0, y, 0), app.Vector(100, y, 0)], label="Outside Candidate {0}".format(index + 1), project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, -40, 0), app.Vector(100, -40, 0)], label="Inside Cabinet Path", project_uuid="project-1", kind="UserPath", ) boundary = doc.addObject("Part::Feature", "CabinetInteriorBoundary") boundary.Shape = FakeShape(FakeBoundBox(-10, 110, -50, 50, -10, 10)) boundary.QetRoutingBoundaryKind = "CabinetInterior" 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("Inside Cabinet Path", labels) self.assertTrue(all(not label.startswith("Outside Candidate") for label in labels)) self.assertEqual(0, result["network"]["route_candidate_boundary_violations"]) def test_eplan_connection_route_tolerates_missing_route_constraint_collector(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="主路径", project_uuid="project-1", kind="UserPath", ) collector = routing_network.collect_route_constraint_options delattr(routing_network, "collect_route_constraint_options") try: result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) finally: routing_network.collect_route_constraint_options = collector self.assertEqual("Routed", result["route_status"]) self.assertEqual({}, result["network"].get("route_constraints", {})) def test_eplan_connection_route_avoids_forbidden_carrier_label(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="禁止路径", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="允许路径", project_uuid="project-1", kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "terminal_exit_length": 0.0, "lane_spacing": 0.0, "forbidden_route_carrier_labels": ["禁止路径"], }, ) labels = [ segment["carrier"]["label"] for segment in result["route_track"]["segments"] ] self.assertIn("允许路径", labels) self.assertNotIn("禁止路径", labels) def test_eplan_connection_route_avoids_carrier_marked_forbidden(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)) forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="近路径", project_uuid="project-1", kind="UserPath", ) forbidden.QetRouteConstraintMode = "Forbidden" routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="远路径", project_uuid="project-1", kind="UserPath", ) 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("远路径", labels) self.assertNotIn("近路径", labels) self.assertIn( forbidden.Name, result["network"]["route_constraints"]["forbidden"]["names"], ) def test_eplan_connection_route_accepts_chinese_constraint_mode_aliases(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)) forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="近路径", project_uuid="project-1", kind="UserPath", ) forbidden.QetRouteConstraintMode = "禁止经过" routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="远路径", project_uuid="project-1", kind="UserPath", ) 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("远路径", labels) self.assertNotIn("近路径", labels) self.assertIn( forbidden.Name, result["network"]["route_constraints"]["forbidden"]["names"], ) def test_eplan_connection_route_uses_carrier_marked_required(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="近路径", project_uuid="project-1", kind="UserPath", ) required = routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="远路径", project_uuid="project-1", kind="UserPath", ) required.QetRouteConstraintMode = "Required" 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("远路径", labels) self.assertNotIn("近路径", labels) self.assertIn( required.Name, result["network"]["route_constraints"]["required"]["names"], ) def test_source_required_constraint_from_multi_wire_sketch_accepts_one_generated_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(100, 0, 0)) route_path = doc.addObject("Sketcher::SketchObject", "YellowMainRouteSketch") route_path.Label = "黄色主路径" route_path.Shape = FakeShape( FakeBoundBox(0, 100, 0, 80, 20, 20), wires=[ FakeWire([app.Vector(0, 0, 20), app.Vector(100, 0, 20)]), FakeWire([app.Vector(0, 80, 20), app.Vector(100, 80, 20)]), ], ) selection = [FakeSelectionItem(obj=route_path)] routing_network.mark_route_constraint_mode_from_selection(doc, selection, "Required") carriers = routing_network.create_user_path_carriers_from_selection( doc, selection, project_uuid="project-1", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) route_carrier_names = [ segment["carrier"]["name"] for segment in result["route_track"]["segments"] if not segment.get("is_bridge") ] self.assertEqual("network-dijkstra-v1", result["algorithm"]) self.assertIn(carriers[0].Name, route_carrier_names) self.assertNotIn(carriers[1].Name, route_carrier_names) self.assertEqual( ["黄色主路径"], result["network"]["route_constraints"]["required"]["source_labels"], ) def test_eplan_connection_route_requires_carrier_label(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="普通路径", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="必经路径", project_uuid="project-1", kind="UserPath", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "terminal_exit_length": 0.0, "lane_spacing": 0.0, "required_route_carrier_labels": ["必经路径"], }, ) labels = [ segment["carrier"]["label"] for segment in result["route_track"]["segments"] ] self.assertIn("必经路径", labels) self.assertNotIn("普通路径", labels) self.assertEqual( ["必经路径"], result["network"]["route_constraints"]["required"]["labels"], ) def test_eplan_connection_route_reports_unsatisfied_route_constraints(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="普通路径", project_uuid="project-1", kind="UserPath", ) with self.assertRaises(auto_routing.AutoRoutingError) as context: auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "terminal_exit_length": 0.0, "required_route_carrier_labels": ["不存在的必经路径"], }, ) self.assertIn("路径约束", str(context.exception)) 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)) parent = doc.addObject("App::Part", "DoorAssembly") parent.Label = "FRONT DOOR-R ASS'Y" parent.addObject(obstacle) 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"]) self.assertEqual(["FRONT DOOR-R ASS'Y"], result["collisions"][0]["obstacle_parent_labels"]) self.assertEqual(["DoorAssembly"], result["collisions"][0]["obstacle_parent_names"]) self.assertEqual("1", result["wire"].QetRouteCollisionCount) self.assertEqual("1", result["wire"].QetRouteHardIntersectionCount) self.assertEqual("0", result["wire"].QetRouteClearanceWarningCount) self.assertEqual("HardIntersectionWarning", result["wire"].QetRouteCollisionStatus) def test_eplan_connection_route_locally_detours_terminal_access_around_third_party_device(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, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "ThirdPartyDevice") obstacle.Label = "第三方设备" terminal_objects.ensure_string_property( obstacle, "QetElementUuid", "QET Exchange", "", "device-obstacle", ) obstacle.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={ "avoid_obstacles": False, "avoid_local_access_obstacles": True, "terminal_exit_length": 0.0, }, endpoint_metadata={ "start_element_uuid": "device-start", "end_element_uuid": "device-end", }, ) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) def test_network_route_limits_local_access_obstacles_to_nearby_bboxes(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, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], project_uuid="project-1", kind="WireDuct", ) near_obstacle = doc.addObject("Part::Feature", "NearThirdPartyDevice") near_obstacle.Label = "近处第三方设备" terminal_objects.ensure_string_property( near_obstacle, "QetElementUuid", "QET Exchange", "", "device-near-obstacle", ) near_obstacle.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) for index in range(120): far_obstacle = doc.addObject("Part::Feature", "FarDevice{0}".format(index)) far_obstacle.Shape = FakeShape( FakeBoundBox(10000 + index * 20, 10010 + index * 20, 10000, 10010, 10000, 10010) ) calls = {"count": 0} original_segment_intersects_bbox = auto_routing._segment_intersects_bbox def counted_segment_intersects_bbox(start_point, end_point, bbox): calls["count"] += 1 return original_segment_intersects_bbox(start_point, end_point, bbox) auto_routing._segment_intersects_bbox = counted_segment_intersects_bbox try: result = auto_routing.build_network_route( start, end, options={ "avoid_obstacles": False, "avoid_local_access_obstacles": True, "terminal_exit_length": 0.0, }, doc=doc, ) finally: auto_routing._segment_intersects_bbox = original_segment_intersects_bbox self.assertIsNotNone(result) self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) self.assertLess(calls["count"], 80) def test_network_route_ignores_unbound_structural_bboxes_for_local_access_avoidance(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, 100, 0)) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 50), app.Vector(100, 0, 50)], project_uuid="project-1", kind="WireDuct", ) near_device = doc.addObject("Part::Feature", "BoundNearDevice") terminal_objects.ensure_string_property( near_device, "QetElementUuid", "QET Exchange", "", "device-near-obstacle", ) near_device.Shape = FakeShape(FakeBoundBox(90, 110, 40, 60, -10, 60)) for index in range(80): cabinet_part = doc.addObject("Part::Feature", "ImportedCabinetPart{0}".format(index)) cabinet_part.Shape = FakeShape(FakeBoundBox(-1000, 1000, -1000, 1000, -1000, 1000)) calls = {"count": 0} original_segment_intersects_bbox = auto_routing._segment_intersects_bbox def counted_segment_intersects_bbox(start_point, end_point, bbox): calls["count"] += 1 return original_segment_intersects_bbox(start_point, end_point, bbox) auto_routing._segment_intersects_bbox = counted_segment_intersects_bbox try: result = auto_routing.build_network_route( start, end, options={ "avoid_obstacles": False, "avoid_local_access_obstacles": True, "terminal_exit_length": 0.0, }, doc=doc, ) finally: auto_routing._segment_intersects_bbox = original_segment_intersects_bbox self.assertIsNotNone(result) self.assertTrue(any(abs(point.x - 75.0) <= 0.001 for point in result["points"])) self.assertLess(calls["count"], 80) def test_network_route_caps_extra_entry_candidates_in_batch_mode(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 index in range(8): routing_network.create_route_carrier( doc, [app.Vector(0, index * 10, 50), app.Vector(100, index * 10, 50)], project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "BoundNearDevice") terminal_objects.ensure_string_property( obstacle, "QetElementUuid", "QET Exchange", "", "device-near-obstacle", ) obstacle.Shape = FakeShape(FakeBoundBox(15, 25, -5, 5, 40, 60)) calls = {"shortest_path": 0} original_shortest_path = routing_network.shortest_path_with_carriers def counted_shortest_path(*args, **kwargs): calls["shortest_path"] += 1 return original_shortest_path(*args, **kwargs) routing_network.shortest_path_with_carriers = counted_shortest_path try: result = auto_routing.build_network_route( start, end, options={ "network_entry_candidate_limit": 3, "network_entry_candidate_total_limit": 4, "avoid_obstacles": False, "avoid_local_access_obstacles": True, "terminal_exit_length": 0.0, }, doc=doc, ) finally: routing_network.shortest_path_with_carriers = original_shortest_path self.assertIsNotNone(result) self.assertLessEqual(calls["shortest_path"], 16) 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)], label="主线槽A", 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"]) self.assertEqual("1", result["wire"].QetRouteCollisionCount) self.assertEqual("0", result["wire"].QetRouteHardIntersectionCount) self.assertEqual("1", result["wire"].QetRouteClearanceWarningCount) self.assertEqual("ClearanceWarning", result["wire"].QetRouteCollisionStatus) 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_explicit_start_local_route_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)) start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") start.QetTerminalLocalRoutePointsJson = json.dumps( [[0, 0, 0], [20, 0, 0], [20, 40, 0]] ) routing_network.create_route_carrier( doc, [app.Vector(20, 80, 0), app.Vector(120, 80, 0)], label="Cabinet Main Path", project_uuid="project-1", ) local_body = doc.addObject("Part::Feature", "StartDeviceLocalShell") local_body.Shape = FakeShape(FakeBoundBox(15, 25, 15, 25, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, ) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) diagnostics = json.loads(result["wire"].QetRouteDiagnosticsJson) self.assertEqual(3, len(diagnostics["endpoint_access"]["start_points"])) def test_eplan_connection_route_still_reports_main_path_collision_after_local_route(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)) start.addProperty("App::PropertyString", "QetTerminalLocalRoutePointsJson", "QET Routing", "") start.QetTerminalLocalRoutePointsJson = json.dumps( [[0, 0, 0], [20, 0, 0], [20, 40, 0]] ) routing_network.create_route_carrier( doc, [app.Vector(20, 80, 0), app.Vector(120, 80, 0)], label="Cabinet Main Path", project_uuid="project-1", ) main_obstacle = doc.addObject("Part::Feature", "MainPathObstacle") main_obstacle.Shape = FakeShape(FakeBoundBox(55, 65, 75, 85, -5, 5)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, ) self.assertEqual("CollisionWarning", result["route_status"]) self.assertEqual(1, result["collision_count"]) self.assertEqual("MainPathObstacle", result["collisions"][0]["obstacle_name"]) def test_eplan_connection_route_detours_local_access_segment_around_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, 100), app.Vector(100, 0, 100)], label="主线槽A", project_uuid="project-1", ) obstacle = doc.addObject("Part::Feature", "AccessObstacle") obstacle.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, 40, 60)) result = auto_routing.route_eplan_connection_between_terminals(doc, start, end) self.assertEqual("Routed", result["route_status"]) self.assertEqual(0, result["collision_count"]) self.assertTrue( any(abs(point.x) > 5.0 or abs(point.y) > 5.0 for point in result["points"]), "局部接入段应增加侧向绕障拐点,而不是直接穿过障碍盒。", ) 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", "end_element_uuid": "device-missing", "end_instance_id": "instance-missing", "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(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"]) self.assertEqual("instance-missing", report["missing_endpoint_samples"][0]["end_instance_id"]) self.assertEqual("A1", report["missing_endpoint_samples"][0]["end_terminal_display"]) self.assertEqual(0, report["missing_endpoint_samples"][0]["end_element_terminal_count"]) self.assertEqual([], report["missing_endpoint_samples"][0]["end_element_terminal_samples"]) self.assertEqual(0, report["missing_endpoint_samples"][0]["end_instance_terminal_count"]) self.assertEqual([], report["missing_endpoint_samples"][0]["end_instance_terminal_samples"]) self.assertEqual( "device_not_in_3d_scene", report["missing_endpoint_samples"][0]["end_missing_endpoint_reason_code"], ) self.assertIn("终点 element=device-missing, instance=instance-missing, terminal=A1", message) self.assertIn("原因=该 2D 设备未在 FreeCAD 场景中找到", message) def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_payload_devices(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 = { "devices": [ { "element_uuid": "device-missing", "instance_id": "instance-from-device-list", "display_tag": "UD:8", "terminals": [ { "terminal_uuid": "device-missing:terminal-a", "terminal_display": "A1", } ], } ], "wires": [ { "wire_id": "wire-1", "wire_mark": "N-MISS", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "device-missing:terminal-a", "end_element_uuid": "device-missing", "end_terminal_display": "A1", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) sample = report["missing_endpoint_samples"][0] self.assertEqual("instance-from-device-list", sample["end_instance_id"]) self.assertEqual("UD:8", sample["end_device_label"]) self.assertEqual( {"device_not_in_3d_scene": 1}, report["missing_terminal_summary"]["reason_code_counts"], ) self.assertEqual(1, len(report["missing_terminal_summary"]["device_groups"])) self.assertEqual("UD:8", report["missing_terminal_summary"]["device_groups"][0]["device_label"]) self.assertEqual(["A1"], report["missing_terminal_summary"]["device_groups"][0]["terminal_displays"]) self.assertIn("UD:8", message) self.assertIn("需补端子设备:UD:8 缺 1 处(A1)", message) self.assertIn("instance=instance-from-device-list", message) def test_route_eplan_connections_backfills_missing_endpoint_device_info_from_context_json_devices(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)) with tempfile.TemporaryDirectory() as temp_dir: json_path = Path(temp_dir) / "2d_to_3d.json" json_path.write_text( json.dumps( { "project_uuid": "project-1", "devices": [ { "element_uuid": "device-missing", "instance_id": "instance-from-context-json", "display_tag": "UD:8", } ], "wires": [ { "wire_id": "wire-1", "wire_mark": "N-MISS", "wire_style_id": "1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "device-missing:terminal-a", "end_element_uuid": "device-missing", "end_terminal_display": "A1", } ], }, ensure_ascii=False, ), encoding="utf-8", ) app._qet_exchange_summary = {"json_path": str(json_path)} payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_mark": "N-MISS", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "device-missing:terminal-a", "end_element_uuid": "device-missing", "end_terminal_display": "A1", } ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) sample = report["missing_endpoint_samples"][0] self.assertEqual("instance-from-context-json", sample["end_instance_id"]) self.assertEqual("UD:8", sample["end_device_label"]) self.assertEqual( "instance-from-context-json", report["missing_terminal_summary"]["device_groups"][0]["instance_id"], ) self.assertTrue(report["context_devices_loaded"]) self.assertEqual(1, report["context_device_count"]) self.assertEqual(str(json_path), report["context_devices_json_path"]) def test_route_eplan_connections_from_payload_reports_device_without_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, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) device = doc.addObject("App::DocumentObjectGroup", "QETDevice_without_terminals") terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-no-terminals") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-no-terminals") payload = { "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", "end_element_uuid": "device-no-terminals", "end_instance_id": "instance-no-terminals", "end_terminal_display": "A1", } ] } report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) sample = report["missing_endpoint_samples"][0] self.assertEqual("QETDevice_without_terminals", sample["end_device_name"]) self.assertTrue(sample["end_device_in_scene"]) self.assertEqual("no_3d_terminals_for_element", sample["end_missing_endpoint_reason_code"]) self.assertIn("原因=该 2D 设备在 FreeCAD 中没有工程端子", message) def test_route_eplan_connections_from_payload_reports_missing_device_binding_metadata(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", "end_terminal_display": "A1", } ] } report = auto_routing.route_eplan_connections_from_payload(doc, payload) message = auto_routing.format_eplan_connection_route_report(report) sample = report["missing_endpoint_samples"][0] self.assertEqual("missing_device_binding_metadata", sample["end_missing_endpoint_reason_code"]) self.assertEqual("导线端点缺少 2D/3D 设备绑定信息", sample["end_missing_endpoint_reason_label"]) self.assertIn("QET 导线端点缺少 element_uuid", message) self.assertIn("第一版不要求 start/end_instance_id", message) self.assertIn("原因=导线端点缺少 2D/3D 设备绑定信息", message) def test_route_eplan_connections_from_payload_applies_per_wire_required_route(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)], label="普通路径", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="必经路径", project_uuid="project-1", kind="UserPath", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-required", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "required_route_carrier_labels": ["必经路径"], } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] for segment in report["routes"][0]["route_track"]["segments"] ] self.assertIn("必经路径", labels) self.assertNotIn("普通路径", labels) def test_route_eplan_connections_from_payload_applies_per_wire_required_source_name(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)) direct = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="普通路径", project_uuid="project-1", kind="UserPath", ) direct.QetRouteSourceName = "NormalSketch" required = routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="黄色主路径", project_uuid="project-1", kind="UserPath", ) required.QetRouteSourceName = "RequiredSketch" required.QetRouteSourceLabel = "黄色主路径草图" payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-required-source", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "required_route_carrier_source_names": ["RequiredSketch"], } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] for segment in report["routes"][0]["route_track"]["segments"] ] self.assertIn("黄色主路径", labels) self.assertNotIn("普通路径", labels) self.assertEqual( ["RequiredSketch"], report["routes"][0]["network"]["route_constraints"]["required"]["source_names"], ) def test_route_eplan_connections_from_payload_applies_per_wire_forbidden_route(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)], label="禁止路径", project_uuid="project-1", kind="UserPath", ) routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="允许路径", project_uuid="project-1", kind="UserPath", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-forbidden", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "forbidden_route_carrier_labels": ["禁止路径"], } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] for segment in report["routes"][0]["route_track"]["segments"] ] self.assertIn("允许路径", labels) self.assertNotIn("禁止路径", labels) def test_route_eplan_connections_from_payload_applies_per_wire_forbidden_source_name(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)) forbidden = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="禁止源路径", project_uuid="project-1", kind="UserPath", ) forbidden.QetRouteSourceName = "ForbiddenSketch" allowed = routing_network.create_route_carrier( doc, [app.Vector(0, 40, 20), app.Vector(100, 40, 20)], label="允许源路径", project_uuid="project-1", kind="UserPath", ) allowed.QetRouteSourceName = "AllowedSketch" payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-forbidden-source", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "forbidden_route_carrier_source_names": ["ForbiddenSketch"], } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) labels = [ segment["carrier"]["label"] for segment in report["routes"][0]["route_track"]["segments"] ] self.assertIn("允许源路径", labels) self.assertNotIn("禁止源路径", labels) self.assertEqual( ["ForbiddenSketch"], report["routes"][0]["network"]["route_constraints"]["forbidden"]["source_names"], ) def test_route_eplan_connections_from_payload_classifies_unsatisfied_route_constraints(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)], label="普通路径", project_uuid="project-1", kind="UserPath", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-unsatisfied", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "required_route_carrier_labels": ["不存在的必经路径"], } ], } report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={"terminal_exit_length": 0.0}, ) self.assertEqual(0, report["routed"]) self.assertEqual(1, report["skipped_missing_route_network"]) self.assertEqual(1, report["route_status_counts"]["MissingRouteNetwork"]) self.assertIn("路径约束", report["missing_route_network_samples"][0]["error"]) 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_route_eplan_connection_tasks_auto_creates_diagnostic_bridge_before_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") _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", label="安装板兜底路径", ) wiring_objects.create_wire_task( doc, "project-1", "wire-task-bridge", "N1", "terminal-start", "terminal-end", "instance-a", "instance-b", ) original_diagnostic = routing_network.diagnose_routing_path_network original_create = routing_network.create_user_path_bridges_from_diagnostic_suggestions calls = {"diagnostic": 0} def fake_diagnostic(*_args, **_kwargs): calls["diagnostic"] += 1 if calls["diagnostic"] == 1: return { "ok": False, "issues": [ { "severity": "warning", "code": "wire_ducts_without_terminal_access", "count": 1, }, ], "summary": {"carriers": 1}, "wire_ducts_without_terminal_access": [ { "index": 0, "carrier_names": ["孤立线槽"], "bridge_suggestion": {"distance_mm": 40.0}, }, ], } return {"ok": True, "issues": [], "summary": {"carriers": 2}} def fake_create(_doc, _diagnostic, project_uuid=""): carrier = routing_network.create_route_carrier( _doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid=project_uuid or "project-1", kind="WireDuct", label="诊断桥接后主路径", ) return {"suggestions": 1, "created": [carrier], "duplicates": 0, "stale_suggestions": 0} routing_network.diagnose_routing_path_network = fake_diagnostic routing_network.create_user_path_bridges_from_diagnostic_suggestions = fake_create try: report = auto_routing.route_eplan_connection_tasks( doc, options={"auto_create_diagnostic_bridges": True}, ) finally: routing_network.diagnose_routing_path_network = original_diagnostic routing_network.create_user_path_bridges_from_diagnostic_suggestions = original_create self.assertEqual(1, report["auto_diagnostic_bridges"]["created_count"]) self.assertEqual({"main_path_routes": 1, "fallback_routes": 0}, report["route_path_usage"]) self.assertEqual(["Routed"], list(report["route_status_counts"].keys())) self.assertNotIn("main_path_not_used", report["issue_codes"]) 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_wire_records_fallback_route_quality_warning(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)], label="安装板兜底路径", project_uuid="project-1", kind="RoutingRange", ) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"terminal_exit_length": 0.0, "lane_spacing": 0.0}, ) wire = result["wire"] self.assertEqual("FallbackPathWarning", wire.QetRouteQualityStatus) self.assertEqual("RoutingRange", wire.QetRouteFallbackCarrierKinds) self.assertEqual("安装板兜底路径", wire.QetRouteFallbackCarrierLabels) self.assertEqual("route_quality_warnings", wire.QetRouteIssueCodes) self.assertEqual("路径质量告警", wire.QetRouteIssueLabels) payload = json.loads(wire.QetRouteDiagnosticsJson) self.assertEqual(["route_quality_warnings"], payload["issue_codes"]) self.assertEqual(["路径质量告警"], payload["issue_labels"]) self.assertEqual("FallbackPathWarning", payload["quality"]["quality_status"]) self.assertEqual(["RoutingRange"], payload["quality"]["fallback_carrier_kinds"]) def test_eplan_connection_wire_records_third_party_collision_issue(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)], label="线槽主路径", project_uuid="project-1", kind="WireDuct", ) obstacle = doc.addObject("Part::Feature", "ThirdPartyDevice") obstacle.Label = "设备A" terminal_objects.ensure_string_property( obstacle, "QetElementUuid", "QET Exchange", "", "device-obstacle", ) obstacle.Shape = FakeShape(FakeBoundBox(50, 70, -10, 10, 15, 25)) result = auto_routing.route_eplan_connection_between_terminals( doc, start, end, options={"avoid_obstacles": False, "terminal_exit_length": 0.0}, endpoint_metadata={ "start_element_uuid": "device-start", "end_element_uuid": "device-end", }, ) wire = result["wire"] self.assertIn("collision_warnings", wire.QetRouteIssueCodes) self.assertIn("third_party_device_collisions", wire.QetRouteIssueCodes) self.assertIn("第三方设备/布局碰撞", wire.QetRouteIssueLabels) payload = json.loads(wire.QetRouteDiagnosticsJson) self.assertIn("third_party_device_collisions", payload["issue_codes"]) self.assertEqual( "third_party_device_collision", payload["collisions"][0]["collision_relation"], ) def test_collision_relation_marks_endpoint_device_collision(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() relation = auto_routing._collision_relation( { "obstacle_element_uuid": "device-start", "start_element_uuid": "device-start", "end_element_uuid": "device-end", } ) self.assertEqual("endpoint_device_collision", relation) def test_unbound_structural_collision_can_be_auto_ignored_without_ignoring_devices(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() structural = { "obstacle_label": "NFB BRACKET_P00", "obstacle_name": "Solid043", "obstacle_element_uuid": "", "obstacle_parent_labels": ["CABINET ASS'Y", "QET Exchange Devices"], "obstacle_parent_names": ["LinkGroup005", "QETExchangeDevices"], } device = { "obstacle_label": "3S001", "obstacle_name": "Device3S001", "obstacle_element_uuid": "device-uuid", "obstacle_parent_labels": ["QET Exchange Devices"], "obstacle_parent_names": ["QETExchangeDevices"], } self.assertTrue(auto_routing._is_auto_ignorable_unbound_structural_collision(structural)) self.assertFalse(auto_routing._is_auto_ignorable_unbound_structural_collision(device)) kept, ignored = auto_routing._filter_auto_ignored_collisions([structural, device]) self.assertEqual([device], kept) self.assertEqual([structural], ignored) 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_from_payload_applies_batch_entry_candidate_limit(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-a", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } captured_options = [] original = auto_routing.route_eplan_connection_between_terminals def fake_route(*args, **kwargs): captured_options.append(dict(kwargs.get("options") or {})) return { "algorithm": "fake", "route_status": "Routed", "length_mm": 10.0, "lane": {"index": 0}, "network": {}, "route_track": {}, "collision_count": 0, "collisions": [], "wire_style_status": "NotRequested", "wire_object_label": "wire-a", } auto_routing.route_eplan_connection_between_terminals = fake_route try: report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={ "network_entry_candidate_limit": 8, "batch_network_entry_candidate_limit": 2, "batch_network_entry_total_candidate_limit": 4, }, ) finally: auto_routing.route_eplan_connection_between_terminals = original self.assertEqual(1, report["routed"]) self.assertEqual(2, report["batch_network_entry_candidate_limit"]) self.assertEqual(4, report["batch_network_entry_total_candidate_limit"]) self.assertFalse(report["batch_avoid_obstacles"]) self.assertEqual(2, captured_options[0]["network_entry_candidate_limit"]) self.assertEqual(4, captured_options[0]["network_entry_candidate_total_limit"]) self.assertFalse(captured_options[0]["avoid_obstacles"]) self.assertIsInstance(captured_options[0]["__base_route_network"], dict) self.assertIsInstance(captured_options[0]["__obstacle_candidate_cache"], dict) def test_route_eplan_connections_retries_missing_route_with_wider_candidate_limit(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-a", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } captured_limits = [] original = auto_routing.route_eplan_connection_between_terminals def fake_route(*args, **kwargs): limit = int((kwargs.get("options") or {}).get("network_entry_candidate_limit", 0) or 0) captured_limits.append(limit) if limit < 8: raise auto_routing.AutoRoutingError( "没有可用的布线路径网络;请先生成布线布局空间和布线路径网络。" ) return { "algorithm": "fake", "route_status": "Routed", "length_mm": 10.0, "lane": {"index": 0}, "network": {}, "route_track": {}, "collision_count": 0, "collisions": [], "wire_style_status": "NotRequested", "wire_object_label": "wire-a", } auto_routing.route_eplan_connection_between_terminals = fake_route try: report = auto_routing.route_eplan_connections_from_payload( doc, payload, options={ "network_entry_candidate_limit": 8, "batch_network_entry_candidate_limit": 3, "missing_route_retry_candidate_limit": 8, }, ) finally: auto_routing.route_eplan_connection_between_terminals = original self.assertEqual([3, 8], captured_limits) self.assertEqual(1, report["routed"]) self.assertEqual(0, report["skipped_missing_route_network"]) self.assertEqual(1, report["missing_route_retries"]) self.assertEqual(1, report["route_status_counts"]["Routed"]) def test_route_eplan_connections_selectively_reroutes_third_party_collisions(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-a", "start_element_uuid": "device-start", "start_terminal_uuid": "terminal-start", "end_element_uuid": "device-end", "end_terminal_uuid": "terminal-end", }, ], } captured_avoid = [] original = auto_routing.route_eplan_connection_between_terminals def fake_route(*args, **kwargs): route_options = dict(kwargs.get("options") or {}) avoid = bool(route_options.get("avoid_obstacles", False)) captured_avoid.append(avoid) if avoid: return { "algorithm": "fake", "route_status": "Routed", "length_mm": 12.0, "lane": {"index": 0}, "network": {}, "route_track": {}, "collision_count": 0, "collisions": [], "wire_style_status": "NotRequested", "wire_object_label": "wire-a clean", } return { "algorithm": "fake", "route_status": "CollisionWarning", "length_mm": 10.0, "lane": {"index": 0}, "network": {}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} ] }, "collision_count": 1, "collisions": [ { "collision_kind": "HardIntersection", "obstacle_element_uuid": "device-obstacle", "obstacle_label": "设备A", } ], "wire_style_status": "NotRequested", "wire_object_label": "wire-a collision", } auto_routing.route_eplan_connection_between_terminals = fake_route try: report = auto_routing.route_eplan_connections_from_payload(doc, payload) finally: auto_routing.route_eplan_connection_between_terminals = original self.assertEqual([False, True], captured_avoid) self.assertEqual(1, report["selective_collision_reroute_attempts"]) self.assertEqual(1, report["selective_collision_reroutes"]) self.assertEqual(0, report["selective_collision_reroute_no_improvement"]) self.assertEqual(1, report["routed"]) self.assertEqual(0, report["collision_warnings"]) self.assertEqual("Routed", report["routes"][0]["route_status"]) def test_route_eplan_connections_rejects_selective_reroute_when_it_uses_fallback_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") _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-a", "start_element_uuid": "device-start", "start_terminal_uuid": "terminal-start", "end_element_uuid": "device-end", "end_terminal_uuid": "terminal-end", }, ], } original = auto_routing.route_eplan_connection_between_terminals created_wires = [] def fake_route(*args, **kwargs): route_doc = args[0] avoid = bool((kwargs.get("options") or {}).get("avoid_obstacles", False)) if avoid: retry_wire = route_doc.addObject("Part::Feature", "RetryFallbackWire") created_wires.append(retry_wire) return { "wire": retry_wire, "algorithm": "fake", "route_status": "Routed", "length_mm": 12.0, "lane": {"index": 0}, "network": {}, "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "辅助面"}} ] }, "collision_count": 0, "collisions": [], "wire_style_status": "NotRequested", "wire_object_label": "wire-a fallback", } original_wire = route_doc.addObject("Part::Feature", "OriginalCollisionWire") created_wires.append(original_wire) return { "wire": original_wire, "algorithm": "fake", "route_status": "CollisionWarning", "length_mm": 10.0, "lane": {"index": 0}, "network": {}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} ] }, "collision_count": 1, "collisions": [ { "collision_kind": "HardIntersection", "obstacle_element_uuid": "device-obstacle", "obstacle_label": "设备A", } ], "wire_style_status": "NotRequested", "wire_object_label": "wire-a collision", } auto_routing.route_eplan_connection_between_terminals = fake_route try: report = auto_routing.route_eplan_connections_from_payload(doc, payload) finally: auto_routing.route_eplan_connection_between_terminals = original self.assertEqual(1, report["selective_collision_reroute_attempts"]) self.assertEqual(0, report["selective_collision_reroutes"]) self.assertEqual(1, report["selective_collision_reroute_rejected_fallback"]) self.assertEqual(1, report["collision_warnings"]) self.assertEqual("CollisionWarning", report["routes"][0]["route_status"]) self.assertEqual("RejectedFallback", report["routes"][0]["selective_collision_reroute_status"]) self.assertEqual( ["RoutingRange"], report["routes"][0]["selective_collision_reroute_rejected_fallback_kinds"], ) self.assertEqual( ["辅助面"], report["routes"][0]["selective_collision_reroute_rejected_fallback_labels"], ) self.assertIn("main_path_detour_missing", report["routes"][0]["issue_codes"]) compact = auto_routing._compact_routing_connection_batch_report(report) self.assertIn("main_path_detour_missing", compact["route_samples"][0]["issue_codes"]) self.assertEqual( ["辅助面"], compact["route_samples"][0]["selective_collision_reroute"]["rejected_fallback_labels"], ) self.assertEqual(1, report["main_path_detour_missing_summary"]["wire_count"]) self.assertEqual( {"辅助面": 1}, report["main_path_detour_missing_summary"]["rejected_fallback_label_counts"], ) self.assertEqual( {"主线槽A": 1}, report["main_path_detour_missing_summary"]["current_route_source_label_counts"], ) self.assertEqual( {"辅助面 -> 主线槽A": 1}, report["main_path_detour_missing_summary"]["bridge_pair_counts"], ) self.assertEqual( ["点击“选择缺主路径补路位置”快速定位汇总需补区域"], [ action for action in report["recommended_actions"] if "选择缺主路径补路位置" in action ], ) self.assertIn("main_path_detour_missing", created_wires[0].QetRouteIssueCodes) wire_payload = json.loads(created_wires[0].QetRouteDiagnosticsJson) self.assertEqual( ["辅助面"], wire_payload["selective_collision_reroute"]["rejected_fallback_labels"], ) self.assertIn("main_path_detour_missing", report["issue_codes"]) message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("局部避障:尝试 1 条,接受 0 条,拒绝辅助路径 1 条", message) self.assertIn("请补主路径/UserPath 或调整装配", message) self.assertIn("缺主路径绕行:1 条,需补路径位置:辅助面 1 条", message) self.assertIn("辅助面 -> 主线槽A 1 条", message) def test_route_eplan_connections_auto_bridges_main_path_detour_pairs_and_retries_once(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)) fallback_source = doc.addObject("Part::Feature", "DoorRoutingRangeSource") fallback_source.Label = "门板布线面" current_source = doc.addObject("Part::Feature", "MainDuctSource") current_source.Label = "主线槽A" fallback_carrier = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], project_uuid="project-1", kind="RoutingRange", label="门板布线面 carrier", ) current_carrier = routing_network.create_route_carrier( doc, [app.Vector(140, 20, 20), app.Vector(240, 20, 20)], project_uuid="project-1", kind="WireDuct", label="主线槽A carrier", ) fallback_carrier.QetRouteSourceName = fallback_source.Name fallback_carrier.QetRouteSourceLabel = fallback_source.Label current_carrier.QetRouteSourceName = current_source.Name current_carrier.QetRouteSourceLabel = current_source.Label payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_element_uuid": "device-start", "start_terminal_uuid": "terminal-start", "end_element_uuid": "device-end", "end_terminal_uuid": "terminal-end", }, ], } original = auto_routing.route_eplan_connection_between_terminals calls = [] def fake_route(*args, **kwargs): route_doc = args[0] calls.append(bool((kwargs.get("options") or {}).get("avoid_obstacles", False))) detour_path_exists = any( getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourPath" for carrier in routing_network.collect_route_carriers(route_doc) ) wire = route_doc.addObject("Part::Feature", "WireAfterDetourPath" if detour_path_exists else "WireBeforeDetourPath") if detour_path_exists: return { "wire": wire, "algorithm": "fake", "route_status": "Routed", "length_mm": 10.0, "lane": {"index": 0}, "network": {}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} ] }, "collision_count": 0, "collisions": [], "wire_style_status": "NotRequested", "wire_object_label": "wire-a routed", } if bool((kwargs.get("options") or {}).get("avoid_obstacles", False)): points = [ app.Vector(0, 0, 0), app.Vector(0, 0, 20), app.Vector(80, 0, 20), app.Vector(140, 20, 20), app.Vector(100, 0, 0), ] wire.Points = points return { "wire": wire, "algorithm": "fake", "route_status": "Routed", "length_mm": 12.0, "lane": {"index": 0}, "network": {}, "points": points, "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} ] }, "collision_count": 0, "collisions": [], "wire_style_status": "NotRequested", "wire_object_label": "wire-a fallback", } return { "wire": wire, "algorithm": "fake", "route_status": "CollisionWarning", "length_mm": 10.0, "lane": {"index": 0}, "network": {}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} ] }, "collision_count": 1, "collisions": [ { "collision_kind": "HardIntersection", "obstacle_element_uuid": "device-obstacle", "obstacle_label": "设备A", } ], "wire_style_status": "NotRequested", "wire_object_label": "wire-a collision", } auto_routing.route_eplan_connection_between_terminals = fake_route try: report = auto_routing.route_eplan_connections( doc, payload=payload, options={"auto_create_main_path_detour_bridges": True}, project_uuid="project-1", update_network=False, ) finally: auto_routing.route_eplan_connection_between_terminals = original bridges = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourBridge" ] detour_paths = [ carrier for carrier in routing_network.collect_route_carriers(doc) if getattr(carrier, "QetRouteBridgeKind", "") == "MainPathDetourPath" ] self.assertEqual(1, report["routed"]) self.assertEqual(0, report["collision_warnings"]) self.assertEqual({"Routed": 1}, report["route_status_counts"]) self.assertEqual(1, report["auto_main_path_detour_bridges"]["created_count"]) self.assertTrue(report["auto_main_path_detour_bridges"]["rerouted"]) self.assertEqual(1, report["auto_main_path_detour_bridges"]["retry_wires"]) self.assertEqual(1, report["auto_main_path_detour_bridges"]["retry_replaced_routes"]) self.assertEqual("门板布线面 -> 主线槽A", bridges[0].QetRouteBridgePairLabel) self.assertEqual("门板布线面 -> 主线槽A", detour_paths[0].QetRouteBridgePairLabel) self.assertEqual([False, True, False], calls) compact = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) self.assertEqual(1, compact["auto_main_path_detour_bridges"]["created_count"]) self.assertIn("自动主路径补桥:生成 UserPath 1 条并重跑布线", message) def test_auto_main_path_detour_user_path_raises_capacity_when_same_path_reused(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") points = [ app.Vector(0, 0, 0), app.Vector(0, 0, 20), app.Vector(100, 0, 20), app.Vector(100, 0, 0), ] retry_result = { "points": points, "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} ] }, } original_result = { "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} ] }, } first = auto_routing._create_main_path_detour_user_path_from_retry( doc, retry_result, original_result, project_uuid="project-1", ) second = auto_routing._create_main_path_detour_user_path_from_retry( doc, retry_result, original_result, project_uuid="project-1", ) self.assertIs(first, second) self.assertEqual(2, first.QetRouteCarrierCapacity) def test_auto_main_path_detour_user_path_initial_capacity_matches_lane_parallel_count(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") retry_result = { "points": [ app.Vector(0, 0, 0), app.Vector(0, 0, 20), app.Vector(100, 0, 20), app.Vector(100, 0, 0), ], "lane": {"index": 1}, "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "门板布线面"}} ] }, } original_result = { "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}} ] }, } carrier = auto_routing._create_main_path_detour_user_path_from_retry( doc, retry_result, original_result, project_uuid="project-1", ) self.assertEqual(2, carrier.QetRouteCarrierCapacity) def test_route_report_raises_auto_detour_path_capacity_from_final_lane_usage(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="UserPath", capacity=1, ) carrier.QetRouteBridgeKind = "MainPathDetourPath" report = { "routes": [ { "wire_uuid": "wire-auto-detour", "route_status": "Routed", "lane": {"index": 1}, "route_track": { "segments": [ { "carrier": { "name": carrier.Name, "kind": "UserPath", "capacity": 1, } } ] }, } ], "skipped_missing_terminal": 0, "skipped_missing_route_network": 0, "skipped_invalid": 0, "errors": [], } auto_routing._raise_main_path_detour_capacities_from_report(doc, report) self.assertEqual(2, carrier.QetRouteCarrierCapacity) self.assertEqual(2, report["routes"][0]["route_track"]["segments"][0]["carrier"]["capacity"]) self.assertEqual([], auto_routing._route_capacity_pressure_samples(report, limit=0)) def test_collect_obstacles_cache_preserves_endpoint_filters(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) endpoint_body = doc.addObject("Part::Feature", "EndpointBody") endpoint_body.Shape = FakeShape(FakeBoundBox(-5, 5, -5, 5, -5, 5)) endpoint_body.QetInstanceId = terminal.QetInstanceId near_body = doc.addObject("Part::Feature", "NearBody") near_body.Shape = FakeShape(FakeBoundBox(1, 2, -1, 1, -1, 1)) far_body = doc.addObject("Part::Feature", "FarBody") far_body.Shape = FakeShape(FakeBoundBox(80, 90, -1, 1, -1, 1)) options = {"terminal_exit_length": 20.0, "obstacle_clearance": 0.0} uncached = auto_routing.collect_obstacles(doc, exclude=[terminal], options=options) cache = auto_routing._obstacle_candidate_cache(doc, options=options) cached = auto_routing.collect_obstacles( doc, exclude=[terminal], options=dict(options, __obstacle_candidate_cache=cache), ) self.assertEqual(["FarBody"], [item["name"] for item in uncached]) self.assertEqual(["FarBody"], [item["name"] for item in cached]) def test_collect_obstacles_skips_parent_of_support_surface_source(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() parent = doc.addObject("App::LinkGroup", "DoorAssembly") parent.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) panel = doc.addObject("Part::Feature", "DoorPanel") panel.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 2)) panel.QetRoutingObstacleMode = "SupportSurface" parent.addObject(panel) obstacle = doc.addObject("Part::Feature", "DeviceObstacle") obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) obstacles = auto_routing.collect_obstacles(doc) cache = auto_routing._obstacle_candidate_cache(doc) cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) def test_collect_obstacles_skips_descendant_of_pass_through_ancestor(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() assembly = doc.addObject("App::LinkGroup", "DoorAssembly") assembly.QetRoutingObstacleMode = "PassThrough" compound = doc.addObject("Part::Compound2", "DoorCompound") panel = doc.addObject("Part::Feature", "DoorPanel") panel.Shape = FakeShape(FakeBoundBox(0, 40, 0, 40, 0, 40)) assembly.addObject(compound) compound.addObject(panel) obstacle = doc.addObject("Part::Feature", "DeviceObstacle") obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) obstacles = auto_routing.collect_obstacles(doc) cache = auto_routing._obstacle_candidate_cache(doc) cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) def test_collect_obstacles_reports_full_parent_chain_for_nested_import_parts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() assembly = doc.addObject("App::LinkGroup", "DoorAssembly") assembly.Label = "FRONT DOOR-R ASS'Y" compound = doc.addObject("Part::Compound2", "DoorCompound") compound.Label = "NAUO141" panel = doc.addObject("Part::Feature", "DoorPanel") panel.Shape = FakeShape(FakeBoundBox(0, 40, 0, 40, 0, 40)) assembly.addObject(compound) compound.addObject(panel) obstacles = auto_routing.collect_obstacles(doc) self.assertEqual(["DoorPanel"], [item["name"] for item in obstacles]) self.assertEqual(["DoorCompound", "DoorAssembly"], obstacles[0]["parent_refs"]["names"]) self.assertEqual(["NAUO141", "FRONT DOOR-R ASS'Y"], obstacles[0]["parent_refs"]["labels"]) def test_collect_obstacles_skips_auto_detected_support_surface_candidate(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() side_cover = doc.addObject("Part::Feature", "SideCover") side_cover.Label = "SIDE COVER-1_P00" side_cover.Shape = FakeShape(FakeBoundBox(0, 600, 0, 2148, 0, 30)) obstacle = doc.addObject("Part::Feature", "DeviceObstacle") obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) obstacles = auto_routing.collect_obstacles(doc) cache = auto_routing._obstacle_candidate_cache(doc) cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) def test_collect_obstacles_skips_outlist_ancestor_of_support_surface_source(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() parent = doc.addObject("App::LinkGroup", "DoorAssembly") parent.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) compound = doc.addObject("Part::Compound2", "DoorCompound") compound.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 20)) panel = doc.addObject("Part::Feature", "DoorPanel") panel.Shape = FakeShape(FakeBoundBox(0, 100, 0, 100, 0, 2)) panel.QetRoutingObstacleMode = "SupportSurface" parent.OutList = [compound] compound.InList = [parent] compound.OutList = [panel] panel.InList = [compound] obstacle = doc.addObject("Part::Feature", "DeviceObstacle") obstacle.Shape = FakeShape(FakeBoundBox(20, 40, 20, 40, 0, 20)) obstacles = auto_routing.collect_obstacles(doc) cache = auto_routing._obstacle_candidate_cache(doc) cached = auto_routing.collect_obstacles(doc, options={"__obstacle_candidate_cache": cache}) self.assertEqual(["DeviceObstacle"], [item["name"] for item in obstacles]) self.assertEqual(["DeviceObstacle"], [item["name"] for item in cached]) 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("N4111", report["missing_route_network_samples"][0]["wire_object_label"]) self.assertEqual([], wiring_objects.iter_routed_wire_objects(doc)) def test_route_eplan_connections_from_payload_attaches_path_diagnostic_when_network_missing(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)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-no-network", "wire_label": "N-NET", "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(0, report["routed"]) self.assertEqual(1, report["skipped_missing_route_network"]) self.assertIn("routing_path_network_diagnostic", report) self.assertFalse(report["routing_path_network_diagnostic"]["ok"]) self.assertTrue(report["routing_path_network_diagnostic"]["issue_codes"]) self.assertEqual(0, report["routing_sources"]["candidate_sources"]) self.assertEqual(0, report["routing_sources"]["route_carriers"]) self.assertIn("路径网络检查提示", message) self.assertIn("未识别到线槽、布线面或用户路径源", message) def test_route_eplan_connections_from_payload_reports_sources_not_generated(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)) panel = doc.addObject("Part::Feature", "MarkedRoutingSource") panel.Label = "已标记布线面" panel.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 5)) panel.QetRoutingSourceKind = "RoutingRange" payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-source-only", "wire_label": "N-SRC", "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(0, report["routed"]) self.assertEqual(1, report["skipped_missing_route_network"]) self.assertEqual(1, report["routing_sources"]["candidate_sources"]) self.assertEqual(0, report["routing_sources"]["route_carriers"]) self.assertEqual({"RoutingRange": 1}, report["routing_sources"]["marked_source_counts"]) self.assertIn("已识别到布线源 1 个,但还没有生成可用路径 carrier", message) 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.assertEqual("project-1", diagnostic.QetProjectUuid) self.assertFalse(diagnostic.QetDiagnosticOk) self.assertIn("missing_terminals", diagnostic.QetDiagnosticIssueCodes) self.assertIn("端子匹配失败", diagnostic.QetDiagnosticIssueLabels) self.assertIn("批量生成布线连接完成", diagnostic.QetDiagnosticMessage) self.assertIn("缺失端子 1 条", diagnostic.QetDiagnosticMessage) self.assertIn("terminal-missing", diagnostic.QetDiagnosticJson) def test_route_eplan_connections_writes_diagnostic_object_when_no_wire_tasks(self): _install_fake_freecad() terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") payload = {"project_uuid": "project-1", "wires": []} report = auto_routing.route_eplan_connections_from_payload(doc, payload) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") self.assertEqual(0, report["total_wires"]) self.assertIn("no_wire_tasks", report["issue_codes"]) self.assertIsNotNone(diagnostic_group) self.assertEqual(1, len(diagnostic_group.Group)) diagnostic = diagnostic_group.Group[0] self.assertEqual("RoutingConnectionBatch", diagnostic.QetDiagnosticKind) self.assertEqual("project-1", diagnostic.QetProjectUuid) self.assertFalse(diagnostic.QetDiagnosticOk) self.assertIn("routed=0", diagnostic.QetDiagnosticMessage) self.assertIn("没有导线任务", diagnostic.QetDiagnosticMessage) diagnostic_payload = json.loads(diagnostic.QetDiagnosticJson) self.assertEqual(0, diagnostic_payload["total_wires"]) self.assertIn("no_wire_tasks", diagnostic_payload["issue_codes"]) 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(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, report["runtime_version"]) self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, diagnostic_payload["runtime_version"]) 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_batch_report_prioritizes_problem_route_samples(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 3, "routed": 3, "routes": [ {"wire_uuid": "normal-a", "route_status": "Routed"}, {"wire_uuid": "normal-b", "route_status": "Routed"}, { "wire_uuid": "problem-collision", "route_status": "CollisionWarning", "collisions": [ { "collision_kind": "HardIntersection", "collision_relation": "third_party_device_collision", } ], }, ], } payload = auto_routing._compact_routing_connection_batch_report( report, sample_limit=2, ) self.assertEqual(3, payload["route_count"]) self.assertEqual(2, payload["route_sample_count"]) self.assertEqual("problem-collision", payload["route_samples"][0]["wire_uuid"]) self.assertEqual( ["collision_warnings", "third_party_device_collisions"], payload["route_samples"][0]["issue_codes"], ) def test_compact_route_sample_includes_wire_object_label(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-label", "wire_label": "N4111", "wire_object_label": "N4111: terminal-start -> terminal-end (Routed)", } ) self.assertEqual( "N4111: terminal-start -> terminal-end (Routed)", sample["wire_object_label"], ) 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_includes_candidate_obstacle_hits(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-obstacle-entry", "network": { "entry_candidate_rank": 3, "exit_candidate_rank": 2, "entry_candidate_score": 125.0, "route_candidate_obstacle_hits": 2, }, } ) self.assertEqual(3, sample["network"]["entry_candidate_rank"]) self.assertEqual(2, sample["network"]["exit_candidate_rank"]) self.assertEqual(125.0, sample["network"]["entry_candidate_score"]) self.assertEqual(2, sample["network"]["route_candidate_obstacle_hits"]) def test_compact_route_sample_includes_candidate_boundary_metadata(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-boundary", "network": { "boundary_aware": True, "route_candidate_boundary_violations": 2, }, } ) self.assertTrue(sample["network"]["boundary_aware"]) self.assertEqual(2, sample["network"]["route_candidate_boundary_violations"]) def test_compact_route_sample_includes_single_wire_status_summaries(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-status-summary", "collisions": [ {"collision_kind": "HardIntersection"}, {"collision_kind": "ClearanceWarning"}, ], "lane": {"index": 2, "spacing_mm": 10.0, "axis": "y"}, "network": { "entry_distance": 125.0, "exit_distance": 20.0, "terminal_access_warning_distance": 100.0, "boundary_aware": True, "route_candidate_boundary_violations": 1, }, "route_track": { "segments": [ { "carrier": { "kind": "RoutingRange", "label": "安装板兜底路径", "capacity": 1, } } ] }, } ) self.assertEqual("LongAccessWarning", sample["access"]["access_status"]) self.assertEqual(["entry"], sample["access"]["warning_sides"]) self.assertEqual("HardIntersectionWarning", sample["collision_summary"]["collision_status"]) self.assertEqual(1, sample["collision_summary"]["hard_intersection_count"]) self.assertEqual(1, sample["collision_summary"]["clearance_warning_count"]) self.assertEqual("FallbackPathWarning", sample["quality"]["quality_status"]) self.assertEqual(["RoutingRange"], sample["quality"]["fallback_carrier_kinds"]) self.assertEqual("CapacityWarning", sample["capacity"]["capacity_status"]) self.assertEqual(3, sample["capacity"]["parallel_wire_count"]) self.assertEqual("BoundaryWarning", sample["boundary"]["boundary_status"]) self.assertEqual( [ "long_terminal_access", "collision_warnings", "route_quality_warnings", "route_capacity_pressure", "route_candidate_boundary_violations", ], sample["issue_codes"], ) self.assertIn("端子接入过长", sample["issue_labels"]) self.assertIn("碰撞告警", sample["issue_labels"]) def test_compact_route_sample_includes_route_constraints(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-constraints", "network": { "route_constraints": { "required": {"labels": ["必经路径"]}, "forbidden": {"labels": ["禁止路径"]}, }, }, } ) self.assertEqual( ["必经路径"], sample["network"]["route_constraints"]["required"]["labels"], ) self.assertEqual( ["禁止路径"], sample["network"]["route_constraints"]["forbidden"]["labels"], ) def test_compact_route_sample_formats_user_path_source_index(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() sample = auto_routing._compact_route_sample( { "wire_uuid": "wire-source-index", "route_track": { "segments": [ { "carrier": { "kind": "UserPath", "source_label": "多路径草图", "source_path_index": "1", } }, { "carrier": { "kind": "UserPath", "source_label": "多路径草图", "source_path_index": "2", } }, ] }, } ) self.assertEqual( ["多路径草图(路径1)", "多路径草图(路径2)"], sample["route_source_labels"], ) 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", "wire_object_label": "N-LONG: T1 -> T2 (Routed)", "network": { "entry_distance": 125.0, "exit_distance": 20.0, }, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "label": "主线槽A"}}, ], }, } ], } 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( "N-LONG: T1 -> T2 (Routed)", payload["route_entry_distance_warning_samples"][0]["wire_object_label"], ) self.assertEqual( ["entry"], payload["route_entry_distance_warning_samples"][0]["warning_sides"], ) self.assertEqual( ["主线槽A"], payload["route_entry_distance_warning_samples"][0]["route_source_labels"], ) def test_compact_batch_report_quality_warning_includes_specific_carrier_labels(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_uuid": "wire-surface", "wire_label": "N-SURFACE", "route_track": { "segments": [ { "carrier": { "kind": "RoutingRange", "label": "安装板辅助路径", } } ], }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( ["安装板辅助路径"], payload["route_quality_warning_samples"][0]["route_carrier_labels"], ) def test_compact_batch_report_includes_candidate_obstacle_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, "routes": [ { "wire_uuid": "wire-obstacle-entry", "wire_label": "N-OBSTACLE", "network": { "route_candidate_obstacle_hits": 2, }, "route_track": { "segments": [ {"carrier": {"kind": "UserPath", "label": "绕行路径A"}}, ], }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["route_candidate_obstacle_warning_count"]) self.assertEqual( "wire-obstacle-entry", payload["route_candidate_obstacle_warning_samples"][0]["wire_uuid"], ) self.assertEqual(2, payload["route_candidate_obstacle_warning_samples"][0]["hits"]) self.assertEqual( ["绕行路径A"], payload["route_candidate_obstacle_warning_samples"][0]["route_source_labels"], ) def test_compact_batch_report_includes_candidate_boundary_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, "routes": [ { "wire_uuid": "wire-outside-cabinet", "wire_label": "N-OUT", "network": { "boundary_aware": True, "route_candidate_boundary_violations": 3, }, "route_track": { "segments": [ {"carrier": {"kind": "UserPath", "label": "柜内主路径A"}}, ], }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["route_candidate_boundary_warning_count"]) self.assertEqual( "wire-outside-cabinet", payload["route_candidate_boundary_warning_samples"][0]["wire_uuid"], ) self.assertEqual( 3, payload["route_candidate_boundary_warning_samples"][0]["violations"], ) self.assertEqual( ["柜内主路径A"], payload["route_candidate_boundary_warning_samples"][0]["route_source_labels"], ) def test_compact_batch_report_includes_route_constraint_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, "routes": [ { "wire_uuid": "wire-constrained", "wire_label": "N-CONSTRAINT", "network": { "route_constraints": { "required": {"labels": ["必经路径"]}, "forbidden": {"labels": ["禁止路径"]}, }, }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["route_constraint_warning_count"]) self.assertEqual( "wire-constrained", payload["route_constraint_warning_samples"][0]["wire_uuid"], ) self.assertEqual( ["必经路径"], payload["route_constraint_warning_samples"][0]["required"]["labels"], ) self.assertEqual( ["禁止路径"], payload["route_constraint_warning_samples"][0]["forbidden"]["labels"], ) def test_compact_batch_report_includes_capacity_pressure_samples(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": [ { "wire_uuid": "wire-crowded", "wire_label": "N-CROWDED", "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "name": "DuctA", "capacity": 2}}, {"carrier": {"kind": "WireDuct", "name": "DuctB", "capacity": 4}}, ] }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["route_capacity_pressure_warning_count"]) self.assertEqual( "wire-crowded", payload["route_capacity_pressure_warning_samples"][0]["wire_uuid"], ) self.assertEqual( 3, payload["route_capacity_pressure_warning_samples"][0]["max_parallel_wires"], ) self.assertEqual( 2, payload["route_capacity_pressure_warning_samples"][0]["min_capacity"], ) self.assertEqual( ["DuctA", "DuctB"], payload["route_capacity_pressure_warning_samples"][0]["carrier_names"], ) def test_compact_batch_report_capacity_pressure_includes_user_path_source_labels(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": [ { "wire_uuid": "wire-crowded", "wire_label": "N-CROWDED", "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ { "carrier": { "kind": "UserPath", "name": "QETRoutePath_001", "capacity": 1, "source_label": "黄色主路径", "source_path_index": "1", } } ] }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( ["黄色主路径(路径1)"], payload["route_capacity_pressure_warning_samples"][0]["route_source_labels"], ) def test_compact_batch_report_includes_collision_kind_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 2, "collision_warnings": 2, "skipped_missing_terminal": 0, "routes": [ { "wire_uuid": "wire-hard", "collision_samples": [ {"collision_kind": "HardIntersection", "obstacle_label": "设备A"}, ], }, { "wire_uuid": "wire-clearance", "collision_samples": [ {"collision_kind": "ClearanceWarning", "obstacle_label": "设备B"}, ], }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["collision_kind_counts"]["HardIntersection"]) self.assertEqual(1, payload["collision_kind_counts"]["ClearanceWarning"]) def test_compact_batch_report_includes_collision_relation_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 2, "collision_warnings": 2, "skipped_missing_terminal": 0, "selective_collision_reroute": True, "selective_collision_reroute_limit": 5, "selective_collision_reroute_attempts": 2, "selective_collision_reroutes": 1, "selective_collision_reroute_no_improvement": 1, "selective_collision_reroute_rejected_fallback": 1, "selective_collision_reroute_errors": 0, "routes": [ { "collision_samples": [ { "collision_kind": "HardIntersection", "collision_relation": "third_party_device_collision", "obstacle_label": "设备A", }, ], }, { "collision_samples": [ { "collision_kind": "ClearanceWarning", "collision_relation": "endpoint_device_collision", "obstacle_label": "设备B", }, ], }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual(1, payload["collision_relation_counts"]["third_party_device_collision"]) self.assertEqual(1, payload["collision_relation_counts"]["endpoint_device_collision"]) self.assertEqual(2, payload["selective_collision_reroute_attempts"]) self.assertEqual(1, payload["selective_collision_reroutes"]) self.assertEqual(1, payload["selective_collision_reroute_no_improvement"]) self.assertEqual(1, payload["selective_collision_reroute_rejected_fallback"]) self.assertIn("third_party_device_collisions", payload["issue_codes"]) self.assertIn("endpoint_device_collisions", payload["issue_codes"]) self.assertIn("main_path_detour_missing", payload["issue_codes"]) self.assertEqual( "selective_local_reroute_or_user_path", payload["collision_reroute_recommendation"]["strategy"], ) self.assertFalse(payload["collision_reroute_recommendation"]["global_avoid_obstacles_recommended"]) self.assertIn("局部", payload["collision_reroute_recommendation"]["reason"]) def test_compact_batch_report_includes_top_collision_obstacles(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 3, "routed": 3, "collision_warnings": 3, "skipped_missing_terminal": 0, "routes": [ { "collision_samples": [ { "collision_kind": "HardIntersection", "obstacle_name": "DeviceAObject", "obstacle_label": "设备A", "obstacle_parent_labels": ["安装板A"], "obstacle_parent_names": ["MountPanelA"], }, { "collision_kind": "ClearanceWarning", "obstacle_name": "DeviceAObject", "obstacle_label": "设备A", "obstacle_parent_labels": ["安装板A"], "obstacle_parent_names": ["MountPanelA"], }, ], }, { "collision_samples": [ { "collision_kind": "HardIntersection", "obstacle_name": "BracketBObject", "obstacle_label": "支架B", "obstacle_parent_labels": ["柜体总成"], "obstacle_parent_names": ["CabinetAssembly"], }, ], }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( [ { "label": "设备A", "name": "DeviceAObject", "count": 2, "collision_kind_counts": { "HardIntersection": 1, "ClearanceWarning": 1, }, "parent_labels": ["安装板A"], "parent_names": ["MountPanelA"], "resolution_hint_code": "review_device_or_layout_collision", "resolution_hint_label": "疑似设备/安装区域碰撞,优先补柜内路径或调整装配", }, { "label": "支架B", "name": "BracketBObject", "count": 1, "collision_kind_counts": {"HardIntersection": 1}, "parent_labels": ["柜体总成"], "parent_names": ["CabinetAssembly"], "resolution_hint_code": "review_pass_through_structural_obstacle", "resolution_hint_label": "疑似柜体/门板/支架结构,确认可穿越后标记忽略碰撞", }, ], payload["top_collision_obstacles"], ) def test_compact_batch_report_summarizes_collision_resolution_categories(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 2, "collision_warnings": 2, "skipped_missing_terminal": 0, "routes": [ { "wire_uuid": "wire-device", "collision_samples": [ { "obstacle_label": "ID:12", "obstacle_name": "QETDevice_A", "collision_kind": "HardIntersection", "obstacle_parent_labels": ["QET Exchange Devices"], } ], }, { "wire_uuid": "wire-structure", "collision_samples": [ { "obstacle_label": "NAUO141", "obstacle_name": "Compound039", "collision_kind": "HardIntersection", "obstacle_parent_labels": ["FRONT DOOR-R ASS'Y"], "obstacle_parent_names": ["DoorAssembly"], }, { "obstacle_label": "支架B", "obstacle_name": "BracketB", "collision_kind": "ClearanceWarning", }, ], }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( { "review_device_or_layout_collision": 1, "review_pass_through_structural_obstacle": 2, }, payload["collision_resolution_summary"]["counts"], ) self.assertEqual( "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;另有 1 个疑似设备/装配碰撞需要补路径或调整装配。", payload["collision_resolution_summary"]["recommended_action"], ) def test_compact_batch_report_issue_codes_include_collision_resolution_categories(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 2, "collision_warnings": 2, "skipped_missing_terminal": 0, "routes": [ { "collision_samples": [ { "obstacle_label": "ID:12", "obstacle_name": "QETDevice_A", "collision_kind": "HardIntersection", } ], }, { "collision_samples": [ { "obstacle_label": "NAUO141", "obstacle_name": "Compound039", "collision_kind": "HardIntersection", "obstacle_parent_labels": ["FRONT DOOR-R ASS'Y"], } ], }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertIn("collision_warnings", payload["issue_codes"]) self.assertIn("device_or_layout_collisions", payload["issue_codes"]) self.assertIn("structural_collision_candidates", payload["issue_codes"]) self.assertIn("设备/布局碰撞", payload["issue_labels"]) self.assertIn("结构件碰撞候选", payload["issue_labels"]) def test_compact_batch_report_issue_codes_include_missing_endpoint_reasons(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 3, "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 2, "missing_endpoint_uuids": ["terminal-missing-a", "terminal-missing-b"], "missing_endpoint_samples": [ { "wire_uuid": "wire-missing-device", "wire_label": "N-MISSING", "start_found": False, "start_terminal_uuid": "terminal-missing-a", "start_element_uuid": "device-a", "start_terminal_display": "A1", "start_missing_endpoint_reason_code": "device_not_in_3d_scene", "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", }, { "wire_uuid": "wire-mismatch", "wire_label": "N-MISMATCH", "end_found": False, "end_terminal_uuid": "terminal-missing-b", "end_element_uuid": "device-b", "end_device_label": "设备B", "end_terminal_display": "B1", "end_missing_endpoint_reason_code": "terminal_uuid_not_in_element", "end_missing_endpoint_reason_label": "同设备存在端子,但没有匹配该 terminal_uuid", }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertIn("missing_terminals", payload["issue_codes"]) self.assertIn("device_not_in_3d_scene", payload["issue_codes"]) self.assertIn("terminal_uuid_not_in_element", payload["issue_codes"]) self.assertIn("3D场景缺少设备", payload["issue_labels"]) self.assertIn("端子UUID不匹配", payload["issue_labels"]) self.assertEqual( { "device_not_in_3d_scene": 1, "terminal_uuid_not_in_element": 1, }, payload["missing_terminal_summary"]["reason_code_counts"], ) self.assertEqual(2, len(payload["missing_terminal_summary"]["device_groups"])) self.assertEqual("device-a", payload["missing_terminal_summary"]["device_groups"][0]["element_uuid"]) self.assertEqual("设备B", payload["missing_terminal_summary"]["device_groups"][1]["device_label"]) def test_routing_diagnostic_recommended_actions_use_collision_resolution_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() summary = { "issue_codes": ["collision_warnings"], "batch_collision_resolution_summary": { "counts": { "review_pass_through_structural_obstacle": 2, "review_device_or_layout_collision": 1, }, "recommended_action": ( "先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough;" "另有 1 个疑似设备/装配碰撞需要补路径或调整装配。" ), }, "diagnostics": { "RoutingConnectionBatch": { "payload": {"collision_warnings": 3}, } }, "routed_wire_issue_summary": {"issue_code_counts": {}}, } actions = auto_routing._routing_diagnostic_recommended_actions(summary) self.assertIn("先处理 2 个疑似结构件碰撞候选:确认后可标记 PassThrough", actions) self.assertIn("另有 1 个疑似设备/装配碰撞需要补路径或调整装配", actions) def test_compact_batch_report_includes_route_path_usage_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "label": "线槽A"}}, ], }, }, { "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, ], }, }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( {"main_path_routes": 1, "fallback_routes": 1}, payload["route_path_usage"], ) def test_compact_batch_report_flags_when_no_main_path_is_used(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, "route_network_carrier_kind_counts": { "WireDuct": 2, "WireDuctOpenEnd": 4, "RoutingRange": 10, }, "routes": [ { "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, ], }, }, { "route_track": { "segments": [ {"carrier": {"kind": "AuxiliaryPath", "label": "门板辅助路径"}}, ], }, }, ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertEqual( {"main_path_routes": 0, "fallback_routes": 2}, payload["route_path_usage"], ) self.assertEqual( {"WireDuct": 2, "WireDuctOpenEnd": 4, "RoutingRange": 10}, payload["route_network_carrier_kind_counts"], ) self.assertEqual(6, payload["route_network_main_path_carriers"]) self.assertIn("main_path_not_used", payload["issue_codes"]) self.assertIn("未使用线槽或用户主路径", payload["issue_labels"]) self.assertIn( "主路径未采用:当前有线槽/UserPath/过线孔路径 6 条,但本批次 2 条导线都走了布线面/辅助路径。", auto_routing.format_eplan_connection_route_report(report), ) def test_route_eplan_connections_report_includes_top_level_path_usage_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, "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-a", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } report = auto_routing.route_eplan_connections_from_payload(doc, payload) self.assertEqual( {"main_path_routes": 1, "fallback_routes": 0}, report["route_path_usage"], ) self.assertEqual([], report["top_collision_obstacles"]) def test_route_eplan_connections_attaches_path_diagnostic_when_main_path_exists_but_unused(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", ) routing_network.create_route_carrier( doc, [app.Vector(500, 0, 20), app.Vector(600, 0, 20)], project_uuid="project-1", kind="WireDuct", label="孤立线槽", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-a", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } original_diagnostic = routing_network.diagnose_routing_path_network def fake_diagnostic(*_args, **_kwargs): return { "ok": False, "issues": [ { "severity": "warning", "code": "wire_ducts_without_terminal_access", "count": 1, }, ], "summary": {"carriers": 2}, "wire_ducts_without_terminal_access": [ { "index": 0, "nodes": 2, "segments": 1, "carrier_kinds": {"WireDuct": 1}, "carrier_names": ["孤立线槽"], "bridge_suggestion": {"distance_mm": 42.0}, }, ], } routing_network.diagnose_routing_path_network = fake_diagnostic try: report = auto_routing.route_eplan_connections_from_payload(doc, payload) finally: routing_network.diagnose_routing_path_network = original_diagnostic self.assertIn("main_path_not_used", report["issue_codes"]) diagnostic = report["routing_path_network_diagnostic"] self.assertIn("wire_ducts_without_terminal_access", diagnostic["issue_codes"]) self.assertEqual( "孤立线槽", diagnostic["wire_ducts_without_terminal_access"][0]["carrier_names"][0], ) 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) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") diagnostics = [ item for item in list(getattr(diagnostic_group, "Group", []) or []) if getattr(item, "QetDiagnosticKind", "") == "RoutingConnectionBatch" ] diagnostic_payload = json.loads(diagnostics[0].QetDiagnosticJson) self.assertEqual(1, diagnostic_payload["hidden_route_carriers"]) self.assertTrue(diagnostic_payload["routing_path_network_updated"] is False) def test_route_eplan_connections_ignores_global_payload_from_other_project(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-current") _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-current", kind="WireDuct", ) wiring_objects.create_wire_task( doc, "project-current", "wire-current", "CURRENT", "terminal-start", "terminal-end", "", "", ) app._qet_exchange_payload = { "project_uuid": "project-old", "wires": [ { "wire_id": "wire-old", "wire_label": "OLD", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.route_eplan_connections(doc, update_network=False) self.assertEqual("project-current", report["project_uuid"]) self.assertEqual(1, report["routed"]) self.assertEqual("wire-current", report["routes"][0]["wire_uuid"]) 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_falls_back_to_carrier_label(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", "label": "手动线槽", "name": "ManualDuct", } }, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径示例:导线 N4111 经过 手动线槽。", 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_report_source_sample_includes_user_path_source_index(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": "UserPath", "source_label": "多路径草图", "source_path_index": "2", } }, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径示例:导线 N4111 经过 多路径草图(路径2)。", message) def test_route_report_source_sample_includes_user_path_source_index_one_when_present(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": "UserPath", "source_label": "用户路径A", "source_path_index": "1", } }, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径示例:导线 N4111 经过 用户路径A(路径1)。", 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_quality_warning_includes_specific_carrier_label(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": "RoutingRange", "label": "安装板辅助路径", } } ], }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("示例 N4111 使用布线面:安装板辅助路径。", message) def test_route_report_includes_main_path_and_fallback_usage_counts(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": "N-WD", "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "label": "线槽A"}}, ], }, }, { "wire_label": "N-RANGE", "route_track": { "segments": [ {"carrier": {"kind": "RoutingRange", "label": "安装板辅助路径"}}, ], }, }, ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径采用:线槽/主路径 1 条,布线面/辅助路径 1 条。", 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_capacity_pressure_includes_sample_wire(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": [ { "wire_label": "N-CROWDED", "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "name": "DuctA", "capacity": 2}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("示例导线 N-CROWDED", message) self.assertIn("DuctA", message) def test_route_report_capacity_pressure_prefers_user_path_source_label(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": [ { "wire_label": "N-CROWDED", "lane": {"index": 2, "spacing_mm": 10.0}, "route_track": { "segments": [ { "carrier": { "kind": "UserPath", "name": "QETRoutePath_001", "capacity": 1, "source_label": "黄色主路径", "source_path_index": "1", } } ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径 黄色主路径(路径1)", 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_includes_missing_route_retry_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 75, "routed": 71, "collision_warnings": 0, "skipped_missing_terminal": 4, "missing_route_retries": 12, "missing_route_retry_candidate_limit": 8, "routes": [], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("候选放宽重试:12 条导线", message) self.assertIn("候选上限 8", 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, }, "route_track": { "segments": [ {"carrier": {"kind": "WireDuct", "label": "主线槽A"}}, ], }, }, { "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) self.assertIn("路径 主线槽A", message) def test_route_report_warns_when_candidate_route_still_hits_obstacles(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": { "route_candidate_obstacle_hits": 2, }, "route_track": { "segments": [ {"carrier": {"kind": "UserPath", "label": "绕行路径A"}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("接入避障提示:1 条导线候选路径仍穿过障碍", message) self.assertIn("示例导线 N1 2 处", message) self.assertIn("路径 绕行路径A", message) def test_route_report_warns_when_candidate_route_still_leaves_cabinet_boundary(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": { "boundary_aware": True, "route_candidate_boundary_violations": 3, }, "route_track": { "segments": [ {"carrier": {"kind": "UserPath", "label": "柜内主路径A"}}, ] }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("柜内边界提示:1 条导线最终路径仍越出柜内区域", message) self.assertIn("示例导线 N1 3 个越界点", message) self.assertIn("路径 柜内主路径A", message) def test_route_report_includes_route_constraint_summary(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": { "route_constraints": { "required": {"labels": ["必经路径"]}, "forbidden": {"labels": ["禁止路径"]}, }, }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("路径约束提示:1 条导线应用必经/禁经规则", message) self.assertIn("示例导线 N1", message) self.assertIn("必须经过 必经路径", message) self.assertIn("禁止经过 禁止路径", message) def test_route_report_prefers_constraint_source_label_over_internal_source_name(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": { "route_constraints": { "required": { "source_names": ["YellowMainRouteSketch"], "source_labels": ["黄色主路径"], }, }, }, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("必须经过 源标签 黄色主路径", message) self.assertNotIn("YellowMainRouteSketch", message) def test_compact_batch_report_does_not_treat_route_constraints_as_issue(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 1, "routed": 1, "collision_warnings": 0, "skipped_missing_terminal": 0, "routes": [ { "wire_uuid": "wire-required", "network": { "route_constraints": { "required": { "source_labels": ["黄色主路径"], }, }, }, } ], } payload = auto_routing._compact_routing_connection_batch_report(report) self.assertNotIn("route_constraints", payload["issue_codes"]) self.assertEqual(1, payload["route_constraint_warning_count"]) 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_path_network_diagnostic_uses_terminal_access_warning_distance(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)) 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, ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-long-access", "wire_label": "N-LONG", "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, "terminal_access_max_distance": 1000.0, "terminal_access_warning_distance": 950.0, }, project_uuid="project-1", update_network=False, ) self.assertNotIn( "long_terminal_accesses", report["routing_path_network_diagnostic"]["issue_codes"], ) def test_route_eplan_connections_path_network_diagnostic_keeps_long_access_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") device = doc.addObject("App::Part", "DevicePEN") device.Label = "PEN" device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) start = _terminal(doc, terminal_objects, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) device.addObject(start) _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(1000, 0, 0)) routing_network.create_route_carrier( doc, [app.Vector(1000, 0, 20), app.Vector(1100, 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, ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-long-access", "wire_label": "N-LONG", "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, "terminal_access_max_distance": 1000.0, }, project_uuid="project-1", update_network=False, ) diagnostic = report["routing_path_network_diagnostic"] self.assertIn("long_terminal_accesses", diagnostic["issue_codes"]) self.assertEqual(1, len(diagnostic["long_terminal_accesses"])) sample = diagnostic["long_terminal_accesses"][0] self.assertEqual("terminal-start", sample["terminal_uuid"]) self.assertEqual("PEN", sample["parent_device_label"]) self.assertEqual("x", sample["terminal_access_dominant_axis"]) self.assertEqual(2, len(sample["terminal_access_points"])) def test_route_report_includes_outside_boundary_path_network_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)) _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) boundary = doc.addObject("Part::Feature", "CabinetBoundary") boundary.Label = "柜内空间" boundary.Shape = FakeShape(FakeBoundBox(0, 120, -20, 20, 0, 80)) routing_network.mark_cabinet_interior_boundaries_from_selection( [FakeSelectionItem(obj=boundary)] ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20), app.Vector(140, 0, 20)], label="柜内主路径A", project_uuid="project-1", kind="UserPath", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-outside-source", "wire_label": "N-OUT", "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", update_network=False, ) message = auto_routing.format_eplan_connection_route_report(report) self.assertIn( "route_carriers_outside_boundary", report["routing_path_network_diagnostic"]["issue_codes"], ) self.assertEqual( "柜内主路径A", report["routing_path_network_diagnostic"]["route_carriers_outside_boundary"][0]["carrier"]["label"], ) self.assertIn("路径网络检查提示:路径越出柜内边界", message) self.assertIn("越界路径:柜内主路径A 1 个越界点", message) def test_route_report_includes_outside_boundary_terminal_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, "TerminalInside", "terminal-inside", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) boundary = doc.addObject("Part::Feature", "CabinetBoundary") boundary.Label = "柜内空间" boundary.Shape = FakeShape(FakeBoundBox(-20, 120, -20, 20, -10, 80)) routing_network.mark_cabinet_interior_boundaries_from_selection( [FakeSelectionItem(obj=boundary)] ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="柜内主路径A", project_uuid="project-1", kind="UserPath", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-outside-terminal", "wire_label": "N-OUT-TERM", "start_terminal_uuid": "terminal-inside", "end_terminal_uuid": "terminal-outside", } ], } report = auto_routing.route_eplan_connections( doc, payload=payload, options={"hide_route_carriers_after_route": False}, project_uuid="project-1", update_network=False, ) message = auto_routing.format_eplan_connection_route_report(report) self.assertIn( "terminals_outside_boundary", report["routing_path_network_diagnostic"]["issue_codes"], ) self.assertEqual( "terminal-outside", report["routing_path_network_diagnostic"]["terminals_outside_boundary"][0]["terminal_uuid"], ) self.assertIn("路径网络检查提示:端子未接入、端子越出柜内边界", message) self.assertIn("越界端子:TerminalOutside(terminal-outside) 2 个越界点", 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("N500", report["error_samples"][0]["wire_object_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, "avoid_local_access_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.assertEqual("1", second_wire.QetRouteLaneIndex) self.assertEqual("y", second_wire.QetRouteLaneAxis) self.assertEqual("10.000", second_wire.QetRouteLaneOffsetMm) self.assertEqual("CapacityWarning", second_wire.QetRouteCapacityStatus) self.assertEqual("2", second_wire.QetRouteParallelWireCount) self.assertEqual("1", second_wire.QetRouteMinCarrierCapacity) self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in second_wire.Points[1:-1])) def test_route_eplan_connections_lane_index_accounts_for_existing_routed_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") start = _terminal(doc, terminal_objects, "TerminalStartA", "terminal-start-a", app.Vector(0, 0, 0)) end = _terminal(doc, terminal_objects, "TerminalEndA", "terminal-end-a", 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", ) auto_routing.route_eplan_connection_between_terminals( doc, start, end, wire_uuid="existing-wire", options={"lane_spacing": 10.0, "lane_axis": "y"}, ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "new-wire", "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(1, report["routes"][0]["lane"]["index"]) routed_group = doc.getObject("QETWiring_04_Routed") new_wire = [ wire for wire in list(getattr(routed_group, "Group", []) or []) if getattr(wire, "QetWireUuid", "") == "new-wire" ][0] self.assertEqual("1", new_wire.QetRouteLaneIndex) self.assertTrue(any(abs(point.y - 10.0) <= 0.001 for point in new_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" terminal_objects.ensure_string_property( obstacle, "QetElementUuid", "QET Exchange", "", "device-obstacle", ) terminal_objects.ensure_string_property( obstacle, "QetInstanceId", "QET Exchange", "", "instance-obstacle", ) obstacle.Shape = FakeShape(FakeBoundBox(40, 60, -10, 10, 90, 110)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "wire_style_id": "style-1", "start_element_uuid": "device-start", "start_terminal_uuid": "terminal-start", "end_element_uuid": "device-end", "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("device-start", report["collision_samples"][0]["start_element_uuid"]) self.assertEqual("device-end", report["collision_samples"][0]["end_element_uuid"]) self.assertEqual( "N4111: terminal-start -> terminal-end (CollisionWarning)", report["collision_samples"][0]["wire_object_label"], ) self.assertEqual("MiddleObstacle", report["collision_samples"][0]["obstacle_name"]) self.assertEqual( "device-obstacle", report["collision_samples"][0]["obstacle_element_uuid"], ) self.assertEqual( "instance-obstacle", report["collision_samples"][0]["obstacle_instance_id"], ) self.assertEqual( "third_party_device_collision", report["collision_samples"][0]["collision_relation"], ) 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.assertEqual( "device-obstacle", report["routes"][0]["collision_samples"][0]["obstacle_element_uuid"], ) self.assertEqual( "instance-obstacle", report["routes"][0]["collision_samples"][0]["obstacle_instance_id"], ) self.assertEqual( "N4111: terminal-start -> terminal-end (CollisionWarning)", report["routes"][0]["collision_samples"][0]["wire_object_label"], ) self.assertEqual( "device-start", report["routes"][0]["collision_samples"][0]["start_element_uuid"], ) self.assertEqual( "device-end", report["routes"][0]["collision_samples"][0]["end_element_uuid"], ) self.assertEqual( "third_party_device_collision", report["routes"][0]["collision_samples"][0]["collision_relation"], ) self.assertEqual(["QET Route Carrier"], report["collision_samples"][0]["route_source_labels"]) self.assertIn("碰撞示例", message) self.assertIn("路径 QET Route Carrier", 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_includes_current_route_network_counts(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 2, "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 0, "prepared_layout": { "wire_duct_carriers": 0, "surface_carriers": 0, "terminal_access_carriers": 4, }, "route_network_carrier_kind_counts": { "WireDuct": 3, "UserPath": 2, "TerminalAccess": 4, }, } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("布线布局空间:线槽路径 0 条,布线面 0 条,端子接入 4 条。", message) self.assertIn("当前路径网络:线槽路径 3 条,用户路径 2 条,端子接入 4 条。", message) def test_route_eplan_connections_report_calls_out_missing_device_when_some_routes_succeed(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 3, "routed": 2, "collision_warnings": 0, "skipped_missing_terminal": 1, "missing_endpoint_samples": [ { "wire_uuid": "wire-missing-device", "wire_label": "F6", "start_found": False, "start_terminal_uuid": "device-a:terminal-as", "start_element_uuid": "device-a", "start_terminal_display": "as", "start_missing_endpoint_reason_code": "device_not_in_3d_scene", "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", "end_found": True, } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("routed=2", message) self.assertIn("设备未在当前 FreeCAD 场景中找到", message) self.assertIn("缺失示例:导线 F6", message) def test_route_eplan_connections_report_tolerates_malformed_total_wires(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": "not-a-number", "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 0, } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("批量生成布线连接完成", message) self.assertIn("没有导线任务", 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) self.assertIn("是否已生成 carrier", message) self.assertIn("路径约束是否过严", message) def test_route_eplan_connections_report_calls_out_zero_routed_after_attempt(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 75, "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 4, "route_network_segments": 1828, "route_network_carriers": 477, "route_network_nodes": 706, "route_status_counts": { "Error": 71, "MissingTerminal": 4, }, "errors": ["module 'RoutingNetwork' has no attribute 'collect_route_constraint_options'"], } issue_codes = auto_routing._routing_connection_batch_issue_codes(report) payload = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("no_routed_connections", issue_codes) self.assertIn("no_routed_connections", payload["issue_codes"]) self.assertIn("未生成有效导线", message) self.assertIn("本次只有路径承载/诊断对象,未生成 RoutedConnection 导线", message) def test_route_eplan_connections_report_treats_error_status_count_as_routing_error(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "total_wires": 75, "routed": 0, "collision_warnings": 0, "skipped_missing_terminal": 4, "route_status_counts": { "Error": 71, "MissingTerminal": 4, }, } issue_codes = auto_routing._routing_connection_batch_issue_codes(report) payload = auto_routing._compact_routing_connection_batch_report(report) message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("routing_errors", issue_codes) self.assertIn("routing_errors", payload["issue_codes"]) self.assertIn("错误 71 条", 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_includes_wire_object_label_for_missing_endpoint(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": [ { "wire_uuid": "wire-missing", "wire_label": "N-MISS", "wire_object_label": "N-MISS: QF1/A1 -> KM1/13 (MissingTerminal)", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn( "缺失示例:导线 N-MISS: QF1/A1 -> KM1/13 (MissingTerminal),terminal-start -> terminal-missing", 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_uses_wire_object_label_for_collision_sample(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_uuid": "wire-collision", "wire_label": "N-COL", "wire_object_label": "N-COL: QF1/A1 -> KM1/13 (CollisionWarning)", "obstacle_label": "柜体侧板", "collision_kind": "HardIntersection", } ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn( "碰撞示例:导线 N-COL: QF1/A1 -> KM1/13 (CollisionWarning) 碰到 柜体侧板", 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_report_includes_collision_kind_summary(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 2, "collision_warnings": 2, "skipped_missing_terminal": 0, "routes": [ { "collision_samples": [ {"collision_kind": "HardIntersection", "obstacle_label": "设备A"}, ], }, { "collision_samples": [ {"collision_kind": "ClearanceWarning", "obstacle_label": "设备B"}, ], }, ], } message = auto_routing.format_eplan_connection_route_report(report) self.assertIn("碰撞分类:硬碰撞 1 处,安全间隙 1 处。", message) def test_route_report_includes_top_collision_obstacles(self): _install_fake_freecad() _terminal_objects, _wiring_objects, _routing_network, auto_routing = _reload_modules() report = { "routed": 3, "collision_warnings": 3, "skipped_missing_terminal": 0, "routes": [ { "collision_samples": [ { "collision_kind": "HardIntersection", "obstacle_label": "设备A", "obstacle_name": "DeviceA", "obstacle_element_uuid": "device-a", "obstacle_instance_id": "instance-a", "collision_relation": "third_party_device_collision", "obstacle_parent_labels": ["安装板A"], }, { "collision_kind": "ClearanceWarning", "obstacle_label": "设备A", "obstacle_name": "DeviceA", "obstacle_element_uuid": "device-a", "obstacle_instance_id": "instance-a", "collision_relation": "third_party_device_collision", "obstacle_parent_labels": ["安装板A"], }, ], }, { "collision_samples": [ {"collision_kind": "HardIntersection", "obstacle_label": "支架B"}, ], }, ], } message = auto_routing.format_eplan_connection_route_report(report) top_obstacles = auto_routing._top_collision_obstacles(report) self.assertEqual("device-a", top_obstacles[0]["element_uuid"]) self.assertEqual("instance-a", top_obstacles[0]["instance_id"]) self.assertEqual( {"third_party_device_collision": 2}, top_obstacles[0]["collision_relation_counts"], ) self.assertIn("碰撞关系:第三方设备/布局 2 处。", message) self.assertIn("后续处理:优先对第三方设备/布局碰撞做局部二次避障", message) self.assertIn("碰撞高发对象:设备A(安装板A) 2 处,支架B 1 处。", message) self.assertIn( "碰撞处理建议:设备A:疑似设备/安装区域碰撞,优先补柜内路径或调整装配;支架B:疑似柜体/门板/支架结构,确认可穿越后标记忽略碰撞。", 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_routing_preflight_reports_missing_route_network_and_style_database(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)) payload = { "project_uuid": "project-1", "wire_style_database_path": "D:/missing/project-local.db", "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "42", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertFalse(report["ok"]) self.assertEqual(1, report["total_wires"]) self.assertEqual(2, report["available_terminals"]) self.assertEqual(0, report["route_network_segments"]) self.assertEqual("Missing", report["wire_style_database"]["status"]) self.assertIn("no_route_network", report["issue_codes"]) self.assertIn("wire_style_database_missing", report["issue_codes"]) self.assertIn("路径网络:0 段", message) self.assertIn("导线样式库:文件不存在", message) def test_routing_preflight_report_identifies_qet_session_payload_source(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)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertEqual("payload", report["source"]) self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, report["runtime_version"]) self.assertIn("导线来源:QET 会话交换数据", message) self.assertIn("运行版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), message) def test_routing_preflight_missing_endpoint_sample_includes_instance_details(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, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-missing", "wire_label": "N-MISS", "start_terminal_uuid": "terminal-missing", "start_element_uuid": "device-missing", "start_instance_id": "instance-missing", "start_terminal_display": "A1", "end_terminal_uuid": "terminal-end", "end_element_uuid": "device-end", "end_instance_id": "instance-end", "end_terminal_display": "13", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) sample = report["missing_endpoint_samples"][0] self.assertEqual("device-missing", sample["start_element_uuid"]) self.assertEqual("instance-missing", sample["start_instance_id"]) self.assertEqual("A1", sample["start_terminal_display"]) self.assertEqual(0, sample["start_element_terminal_count"]) self.assertEqual([], sample["start_element_terminal_samples"]) self.assertEqual(0, sample["start_instance_terminal_count"]) self.assertEqual([], sample["start_instance_terminal_samples"]) self.assertEqual("device_not_in_3d_scene", sample["start_missing_endpoint_reason_code"]) self.assertEqual("该 2D 设备未在 FreeCAD 场景中找到", sample["start_missing_endpoint_reason_label"]) self.assertIn("device-missing/A1 (terminal-missing)", message) self.assertIn("起点 element=device-missing, instance=instance-missing, terminal=A1", message) self.assertIn("FreeCAD同设备端子=0", message) self.assertIn("FreeCAD同实例端子=0", message) self.assertIn("原因=该 2D 设备未在 FreeCAD 场景中找到", message) def test_routing_preflight_reports_missing_runtime_route_constraint_collector(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)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } collector = routing_network.collect_route_constraint_options delattr(routing_network, "collect_route_constraint_options") try: report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) finally: routing_network.collect_route_constraint_options = collector self.assertFalse(report["ok"]) self.assertIn("runtime_route_constraint_collector_missing", report["issue_codes"]) self.assertFalse(report["runtime_capabilities"]["route_constraint_collector"]) self.assertIn("运行模块能力", message) self.assertIn("路径约束收集函数缺失", message) def test_routing_preflight_writes_compact_diagnostic_object(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", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", "wire_style_id": "42", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) first = auto_routing.write_routing_preflight_diagnostic(doc, report) second = auto_routing.write_routing_preflight_diagnostic(doc, report) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") diagnostic_payload = json.loads(second.QetDiagnosticJson) self.assertIsNotNone(first) self.assertIsNotNone(second) self.assertIsNot(first, second) self.assertEqual(1, len(diagnostic_group.Group)) self.assertEqual("RoutingPreflight", diagnostic_group.Group[0].QetDiagnosticKind) self.assertEqual("project-1", diagnostic_group.Group[0].QetProjectUuid) self.assertFalse(diagnostic_group.Group[0].QetDiagnosticOk) self.assertIn("missing_endpoints", diagnostic_group.Group[0].QetDiagnosticIssueCodes) self.assertIn("缺失端点", diagnostic_group.Group[0].QetDiagnosticIssueLabels) self.assertIn("布线准备度:未通过", diagnostic_group.Group[0].QetDiagnosticMessage) self.assertEqual("project-1", diagnostic_payload["project_uuid"]) self.assertEqual(1, diagnostic_payload["total_wires"]) self.assertEqual(1, diagnostic_payload["available_terminals"]) self.assertIn("missing_endpoints", diagnostic_payload["issue_codes"]) self.assertEqual(1, diagnostic_payload["missing_endpoint_uuid_count"]) self.assertEqual("terminal-missing", diagnostic_payload["missing_endpoint_uuids"][0]) self.assertIn( "端点缺失示例:导线 N4111,terminal-start -> terminal-missing", message, ) self.assertIn("routing_sources", diagnostic_payload) self.assertIn("routing_boundaries", diagnostic_payload) self.assertIn("wire_style_database", diagnostic_payload) self.assertIn("wire_style", diagnostic_payload) self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, diagnostic_payload["runtime_version"]) def test_routing_preflight_reports_obstacle_mode_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, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) ignored = doc.addObject("Part::Feature", "IgnoredBracket") ignored.Label = "忽略支架" ignored.Shape = FakeShape(FakeBoundBox(0, 100, 0, 20, 0, 20)) ignored.QetRoutingObstacleMode = "PassThrough" payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_label": "N4111", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertEqual(1, report["routing_obstacle_modes"]["PassThrough"]["count"]) self.assertEqual("忽略支架", report["routing_obstacle_modes"]["PassThrough"]["samples"][0]["label"]) self.assertIn("忽略碰撞对象:1", message) def test_collect_routing_diagnostic_summary_merges_latest_diagnostic_objects(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") for kind, ok, message, payload in ( ( "RoutingPreflight", False, "布线准备度:未通过。", { "issue_codes": ["missing_endpoints"], "total_wires": 2, "runtime_version": "preflight-version", }, ), ( "RoutingPathNetwork", False, "布线路径网络检查发现 1 类问题。", {"issue_codes": ["unconnected_terminals"], "summary": {"segments": 4}}, ), ( "RoutingConnectionBatch", True, "自动布线完成:已生成 2 条。", { "issue_codes": [], "routed": 2, "route_path_usage": {"main_path_routes": 1, "fallback_routes": 1}, "top_collision_obstacles": [ {"label": "设备A", "count": 2, "parent_labels": ["安装板A"]} ], "runtime_version": auto_routing.AUTO_ROUTING_RUNTIME_VERSION, }, ), ): diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_{0}".format(kind)) diagnostic.QetDiagnosticKind = kind diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = ok diagnostic.QetDiagnosticMessage = message diagnostic.QetDiagnosticJson = json.dumps(payload, ensure_ascii=False) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertFalse(summary["ok"]) self.assertEqual("project-1", summary["project_uuid"]) self.assertEqual(3, summary["diagnostic_count"]) self.assertEqual( ["missing_endpoints", "unconnected_terminals"], summary["issue_codes"], ) self.assertEqual([], summary["missing_diagnostic_kinds"]) self.assertEqual(auto_routing.AUTO_ROUTING_RUNTIME_VERSION, summary["runtime_version"]) self.assertEqual(2, summary["diagnostics"]["RoutingConnectionBatch"]["payload"]["routed"]) self.assertEqual( {"main_path_routes": 1, "fallback_routes": 1}, summary["batch_route_path_usage"], ) self.assertEqual( [{"label": "设备A", "count": 2, "parent_labels": ["安装板A"]}], summary["batch_top_collision_obstacles"], ) self.assertIn("汇总诊断:未通过", message) self.assertIn("运行版本:{0}".format(auto_routing.AUTO_ROUTING_RUNTIME_VERSION), message) self.assertIn("路径采用:线槽/主路径 1 条,布线面/辅助路径 1 条。", message) self.assertIn("碰撞高发对象:设备A(安装板A) 2 处。", message) self.assertIn("缺失端点", message) self.assertIn("端子未接入", message) def test_write_routing_diagnostic_summary_replaces_previous_summary_object(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingPreflight") diagnostic.QetDiagnosticKind = "RoutingPreflight" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "布线准备度:未通过。" diagnostic.QetDiagnosticJson = json.dumps( {"issue_codes": ["missing_endpoints"], "total_wires": 2}, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) first = auto_routing.write_routing_diagnostic_summary(doc) second = auto_routing.write_routing_diagnostic_summary(doc) summary_objects = [ item for item in diagnostic_group.Group if getattr(item, "QetDiagnosticKind", "") == "RoutingDiagnosticSummary" ] payload = json.loads(second.QetDiagnosticJson) self.assertIsNotNone(first) self.assertIsNotNone(second) self.assertIsNot(first, second) self.assertEqual(1, len(summary_objects)) self.assertEqual("RoutingDiagnosticSummary", second.QetDiagnosticKind) self.assertEqual("project-1", second.QetProjectUuid) self.assertFalse(second.QetDiagnosticOk) self.assertEqual("missing_endpoints", second.QetDiagnosticIssueCodes) self.assertEqual("缺失端点", second.QetDiagnosticIssueLabels) self.assertIn("汇总诊断:未通过", second.QetDiagnosticMessage) self.assertEqual(["missing_endpoints"], payload["issue_codes"]) self.assertIn("RoutingPathNetwork", payload["missing_diagnostic_kinds"]) self.assertIn("RoutingConnectionBatch", payload["missing_diagnostic_kinds"]) def test_collect_routing_diagnostic_summary_falls_back_to_issue_code_property(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticIssueCodes = "missing_terminals, collision_warnings" diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = "{broken-json" diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) self.assertIn("missing_terminals", summary["issue_codes"]) self.assertIn("collision_warnings", summary["issue_codes"]) self.assertIn("diagnostic_json_invalid", summary["issue_codes"]) def test_collect_routing_diagnostic_summary_accepts_batch_as_final_diagnostic(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = True diagnostic.QetDiagnosticMessage = "批量生成布线连接完成:routed=2, collision_warnings=0, missing_terminals=0" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": [], "routed": 2, "skipped_missing_terminal": 0, "collision_warnings": 0, "route_path_usage": {"main_path_routes": 2, "fallback_routes": 0}, "routing_path_network_diagnostic": {"issue_codes": [], "summary": {"segments": 4}}, "runtime_version": auto_routing.AUTO_ROUTING_RUNTIME_VERSION, }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertTrue(summary["ok"]) self.assertEqual([], summary["missing_diagnostic_kinds"]) self.assertNotIn("未生成", message) self.assertIn("汇总诊断:通过", message) self.assertIn("路径采用:线槽/主路径 2 条,布线面/辅助路径 0 条。", message) def test_collect_routing_diagnostic_summary_reports_empty_diagnostic_json(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticIssueCodes = "" diagnostic.QetDiagnosticMessage = "" diagnostic.QetDiagnosticJson = "" diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn("diagnostic_json_empty", summary["issue_codes"]) self.assertIn("诊断 JSON 为空", summary["issue_labels"]) self.assertIn("诊断 JSON 为空", message) def test_collect_routing_diagnostic_summary_reports_routed_wires_missing_diagnostics(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") wiring_objects.ensure_diagnostic_group(doc, "project-1") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") wire = doc.addObject("Part::Feature", "QETRoutedConnection_legacy") wire.Label = "N-OLD: A1 -> B1" wire.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", ] wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-old" wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteDiagnosticsJson = "" routed_group.addObject(wire) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn("routed_wire_diagnostics_missing", summary["issue_codes"]) self.assertEqual(1, summary["routed_wire_diagnostic_gaps"]["count"]) self.assertEqual("N-OLD: A1 -> B1", summary["routed_wire_diagnostic_gaps"]["samples"][0]["label"]) self.assertIn("导线诊断缺失", summary["issue_labels"]) self.assertIn("导线诊断缺失:1 条", message) self.assertIn("N-OLD: A1 -> B1", message) def test_collect_routing_diagnostic_summary_reports_invalid_routed_wire_diagnostics(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") wiring_objects.ensure_diagnostic_group(doc, "project-1") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") wire = doc.addObject("Part::Feature", "QETRoutedConnection_invalid_diag") wire.Label = "N-BAD: A1 -> B1" wire.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", ] wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-bad" wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteDiagnosticsJson = "{broken-json" routed_group.addObject(wire) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn("routed_wire_diagnostics_invalid", summary["issue_codes"]) self.assertEqual(1, summary["routed_wire_diagnostic_gaps"]["invalid_count"]) self.assertEqual("N-BAD: A1 -> B1", summary["routed_wire_diagnostic_gaps"]["invalid_samples"][0]["label"]) self.assertIn("导线诊断 JSON 无效", summary["issue_labels"]) self.assertIn("导线诊断 JSON 无效:1 条", message) self.assertIn("N-BAD: A1 -> B1", message) def test_collect_routing_diagnostic_summary_reports_main_path_detour_missing_details(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") wiring_objects.ensure_diagnostic_group(doc, "project-1") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") wire_a = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_a") wire_a.Label = "N-A: A1 -> B1" wire_a.RouteType = "RoutedConnection" wire_a.QetWireUuid = "wire-a" wire_a.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", "QetRouteTrackJson", ] wire_a.QetStartTerminalUuid = "terminal-a" wire_a.QetEndTerminalUuid = "terminal-b" wire_a.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" wire_a.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["RoutingRange"], "rejected_fallback_labels": ["安装板布线面", "辅助路径A"], } }, ensure_ascii=False, ) wire_a.QetRouteTrackJson = json.dumps( { "segments": [ {"carrier": {"kind": "WireDuct", "source_label": "主线槽A"}}, ] }, ensure_ascii=False, ) routed_group.addObject(wire_a) wire_b = doc.addObject("Part::Feature", "QETRoutedConnection_main_path_b") wire_b.Label = "N-B: A2 -> B2" wire_b.RouteType = "RoutedConnection" wire_b.QetWireUuid = "wire-b" wire_b.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", "QetRouteTrackJson", ] wire_b.QetStartTerminalUuid = "terminal-c" wire_b.QetEndTerminalUuid = "terminal-d" wire_b.QetRouteIssueCodes = "main_path_detour_missing" wire_b.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["AuxiliaryPath"], "rejected_fallback_labels": ["辅助路径A", "门板附近辅助路径"], } }, ensure_ascii=False, ) wire_b.QetRouteTrackJson = json.dumps( { "segments": [ {"carrier": {"kind": "UserPath", "source_label": "主路径B"}}, ] }, ensure_ascii=False, ) routed_group.addObject(wire_b) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) detour = summary["main_path_detour_missing_summary"] self.assertEqual(2, detour["wire_count"]) self.assertEqual(["安装板布线面", "辅助路径A", "门板附近辅助路径"], detour["rejected_fallback_labels"]) self.assertEqual( {"安装板布线面": 1, "辅助路径A": 2, "门板附近辅助路径": 1}, detour["rejected_fallback_label_counts"], ) self.assertEqual({"AuxiliaryPath": 1, "RoutingRange": 1}, detour["rejected_fallback_kind_counts"]) self.assertEqual({"主线槽A": 1, "主路径B": 1}, detour["current_route_source_label_counts"]) self.assertEqual( { "安装板布线面 -> 主线槽A": 1, "辅助路径A -> 主线槽A": 1, "辅助路径A -> 主路径B": 1, "门板附近辅助路径 -> 主路径B": 1, }, detour["bridge_pair_counts"], ) self.assertEqual(["主线槽A"], detour["samples"][0]["current_route_source_labels"]) self.assertEqual(["N-A: A1 -> B1", "N-B: A2 -> B2"], [item["label"] for item in detour["samples"]]) self.assertIn("缺主路径绕行:2 条", message) self.assertIn("需补路径位置:辅助路径A 2 条、安装板布线面 1 条、门板附近辅助路径 1 条", message) self.assertIn("辅助路径A -> 主线槽A 1 条", message) def test_collect_routing_diagnostic_summary_counts_routed_wire_issue_codes(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") wiring_objects.ensure_diagnostic_group(doc, "project-1") routed_group = wiring_objects.ensure_routed_group(doc, "project-1") long_wire = doc.addObject("Part::Feature", "QETRoutedConnection_long") long_wire.Label = "N-LONG: A1 -> B1" long_wire.RouteType = "RoutedConnection" long_wire.QetWireUuid = "wire-long" long_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] long_wire.QetStartTerminalUuid = "terminal-a" long_wire.QetEndTerminalUuid = "terminal-b" long_wire.QetRouteIssueCodes = "long_terminal_access" routed_group.addObject(long_wire) collision_wire = doc.addObject("Part::Feature", "QETRoutedConnection_collision") collision_wire.Label = "N-COL: A2 -> B2" collision_wire.RouteType = "RoutedConnection" collision_wire.QetWireUuid = "wire-col" collision_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] collision_wire.QetStartTerminalUuid = "terminal-c" collision_wire.QetEndTerminalUuid = "terminal-d" collision_wire.QetRouteIssueCodes = "collision_warnings, route_capacity_pressure" routed_group.addObject(collision_wire) normal_wire = doc.addObject("Part::Feature", "QETRoutedConnection_ok") normal_wire.Label = "N-OK: A3 -> B3" normal_wire.RouteType = "RoutedConnection" normal_wire.QetWireUuid = "wire-ok" normal_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] normal_wire.QetStartTerminalUuid = "terminal-e" normal_wire.QetEndTerminalUuid = "terminal-f" normal_wire.QetRouteIssueCodes = "" routed_group.addObject(normal_wire) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) issue_summary = summary["routed_wire_issue_summary"] self.assertEqual(2, issue_summary["issue_wire_count"]) self.assertEqual(3, issue_summary["total_wire_count"]) self.assertEqual( { "collision_warnings": 1, "long_terminal_access": 1, "route_capacity_pressure": 1, }, issue_summary["issue_code_counts"], ) self.assertEqual("N-LONG: A1 -> B1", issue_summary["samples"][0]["label"]) self.assertIn("异常导线:2/3 条", message) self.assertIn("端子接入过长 1 条", message) self.assertIn("碰撞告警 1 条", message) def test_collect_routing_diagnostic_summary_counts_missing_terminal_reasons(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": ["missing_terminals"], "skipped_missing_terminal": 4, "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "wire_label": "N-A", "start_found": False, "start_terminal_uuid": "terminal-a", "start_element_uuid": "device-a", "start_device_label": "设备A", "start_terminal_display": "A1", "start_missing_endpoint_reason_code": "no_3d_terminals_for_element", "start_missing_endpoint_reason_label": "该 2D 设备在 FreeCAD 中没有工程端子", "end_found": True, }, { "wire_uuid": "wire-b", "wire_label": "N-B", "start_found": False, "start_terminal_uuid": "terminal-b", "start_element_uuid": "device-a", "start_device_label": "设备A", "start_terminal_display": "A2", "start_missing_endpoint_reason_code": "no_3d_terminals_for_element", "start_missing_endpoint_reason_label": "该 2D 设备在 FreeCAD 中没有工程端子", "end_found": False, "end_terminal_uuid": "terminal-c", "end_element_uuid": "device-b", "end_device_label": "设备B", "end_terminal_display": "B1", "end_missing_endpoint_reason_code": "terminal_uuid_not_in_element", "end_missing_endpoint_reason_label": "同设备存在端子,但没有匹配该 terminal_uuid", }, ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) missing_summary = summary["batch_missing_terminal_summary"] self.assertEqual(4, missing_summary["skipped_missing_terminal"]) self.assertEqual(2, missing_summary["sample_wire_count"]) self.assertEqual(3, missing_summary["missing_endpoint_count"]) self.assertEqual( { "no_3d_terminals_for_element": 2, "terminal_uuid_not_in_element": 1, }, missing_summary["reason_code_counts"], ) self.assertEqual(2, len(missing_summary["device_groups"])) self.assertEqual("设备A", missing_summary["device_groups"][0]["device_label"]) self.assertEqual(2, missing_summary["device_groups"][0]["missing_endpoint_count"]) self.assertEqual(["A1", "A2"], missing_summary["device_groups"][0]["terminal_displays"]) self.assertEqual(["terminal-a", "terminal-b"], missing_summary["device_groups"][0]["terminal_uuids"]) self.assertEqual("设备B", missing_summary["device_groups"][1]["device_label"]) self.assertIn("缺端子:4 条", message) self.assertIn("该 2D 设备在 FreeCAD 中没有工程端子 2 处", message) self.assertIn("需补端子设备:设备A 缺 2 处(A1、A2),设备B 缺 1 处(B1)", message) def test_collect_routing_diagnostic_summary_reports_missing_terminal_samples_without_reason_codes(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": ["missing_terminals"], "skipped_missing_terminal": 2, "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-a", "end_found": True, }, { "wire_uuid": "wire-b", "start_found": True, "end_found": False, "end_terminal_uuid": "terminal-b", }, ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) missing_summary = summary["batch_missing_terminal_summary"] self.assertEqual({"missing_device_binding_metadata": 2}, missing_summary["reason_code_counts"]) self.assertEqual({"导线端点缺少 2D/3D 设备绑定信息": 2}, missing_summary["reason_label_counts"]) self.assertIn("导线端点缺少 2D/3D 设备绑定信息 2 处", message) self.assertIn( "检查 QET 导线端点是否提供 element_uuid 和 terminal_uuid(第一版不要求 start/end_instance_id)", summary["recommended_actions"], ) def test_collect_routing_diagnostic_summary_backfills_missing_endpoint_reason_from_old_batch(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": ["missing_terminals"], "skipped_missing_terminal": 1, "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-a", "start_element_uuid": "device-a", "start_instance_id": "instance-a", "start_terminal_display": "A1", "end_found": True, } ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) missing_summary = summary["batch_missing_terminal_summary"] self.assertEqual({"device_not_in_3d_scene": 1}, missing_summary["reason_code_counts"]) self.assertEqual({"该 2D 设备未在 FreeCAD 场景中找到": 1}, missing_summary["reason_label_counts"]) self.assertIn("该 2D 设备未在 FreeCAD 场景中找到 1 处", message) self.assertNotIn("重新生成布线连接,刷新缺端子原因诊断", summary["recommended_actions"]) def test_collect_routing_diagnostic_summary_backfills_issue_codes_from_legacy_missing_batch(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticIssueCodes = "" diagnostic.QetDiagnosticJson = json.dumps( { "skipped_missing_terminal": 1, "route_status_counts": { "Error": 2, "MissingTerminal": 1, }, "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "wire_label": "N1", "start_found": False, "start_terminal_uuid": "terminal-a", "start_element_uuid": "device-a", "start_terminal_display": "A1", "end_found": True, } ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn("missing_terminals", summary["issue_codes"]) self.assertIn("missing_endpoints", summary["issue_codes"]) self.assertIn("routing_errors", summary["issue_codes"]) self.assertIn("端子匹配失败", summary["issue_labels"]) self.assertIn("缺失端点", summary["issue_labels"]) self.assertIn("布线计算错误", summary["issue_labels"]) self.assertEqual({"Error": 2, "MissingTerminal": 1}, summary["batch_route_status_counts"]) self.assertIn("结果状态:错误 2 条,缺失端子 1 条", message) def test_collect_routing_diagnostic_summary_recommends_device_binding_when_3d_device_missing(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": ["missing_terminals"], "skipped_missing_terminal": 4, "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "wire_label": "F6", "start_found": False, "start_terminal_uuid": "device-missing:terminal-a", "start_element_uuid": "device-missing", "start_terminal_display": "as", "start_missing_endpoint_reason_code": "device_not_in_3d_scene", "start_missing_endpoint_reason_label": "该 2D 设备未在 FreeCAD 场景中找到", "end_found": True, } ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn( "检查缺失 3D 设备是否已导入、装配并完成 2D/3D 绑定", summary["recommended_actions"], ) self.assertIn("该 2D 设备未在 FreeCAD 场景中找到 1 处", message) self.assertIn("建议:检查缺失 3D 设备是否已导入", message) def test_collect_routing_diagnostic_summary_recommends_qet_endpoint_binding_metadata(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": ["missing_terminals"], "skipped_missing_terminal": 1, "missing_endpoint_samples": [ { "wire_uuid": "wire-a", "start_found": False, "start_terminal_uuid": "terminal-a", "start_terminal_display": "A1", "start_missing_endpoint_reason_code": "missing_device_binding_metadata", "start_missing_endpoint_reason_label": "导线端点缺少 2D/3D 设备绑定信息", "end_found": True, } ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn( "检查 QET 导线端点是否提供 element_uuid 和 terminal_uuid(第一版不要求 start/end_instance_id)", summary["recommended_actions"], ) self.assertIn("导线端点缺少 2D/3D 设备绑定信息 1 处", message) def test_collect_routing_diagnostic_summary_recommends_manual_followup_actions(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( { "issue_codes": ["missing_terminals", "collision_warnings"], "skipped_missing_terminal": 1, "collision_warnings": 1, "top_collision_obstacles": [ {"label": "NAUO141", "count": 3, "parent_names": ["DoorAssembly"]} ], }, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) routed_group = wiring_objects.ensure_routed_group(doc, "project-1") long_wire = doc.addObject("Part::Feature", "QETRoutedConnection_long") long_wire.Label = "N-LONG: A1 -> B1" long_wire.RouteType = "RoutedConnection" long_wire.QetWireUuid = "wire-long" long_wire.PropertiesList = ["QetStartTerminalUuid", "QetEndTerminalUuid"] long_wire.QetStartTerminalUuid = "terminal-a" long_wire.QetEndTerminalUuid = "terminal-b" long_wire.QetRouteIssueCodes = "long_terminal_access" routed_group.addObject(long_wire) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertEqual( [ "点击“选择缺端子设备”定位需要补工程端子的设备", "点击“选择异常导线”定位带问题码的导线", "点击“选择长接入端子/设备”检查设备高度和局部出线路径", "点击“选择碰撞父装配”确认结构件后再标记忽略碰撞", ], summary["recommended_actions"], ) self.assertIn("建议:点击“选择缺端子设备”", message) self.assertIn("点击“选择碰撞父装配”", message) def test_collect_routing_diagnostic_summary_recommends_main_path_detour_missing_wires(self): _install_fake_freecad() terminal_objects, wiring_objects, _routing_network, auto_routing = _reload_modules() doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") diagnostic_group = wiring_objects.ensure_diagnostic_group(doc, "project-1") diagnostic = doc.addObject("App::DocumentObjectGroup", "Diagnostic_RoutingConnectionBatch") diagnostic.QetDiagnosticKind = "RoutingConnectionBatch" diagnostic.QetProjectUuid = "project-1" diagnostic.QetDiagnosticOk = False diagnostic.QetDiagnosticMessage = "批量生成布线连接完成。" diagnostic.QetDiagnosticJson = json.dumps( {"issue_codes": ["collision_warnings", "main_path_detour_missing"]}, ensure_ascii=False, ) diagnostic_group.addObject(diagnostic) routed_group = wiring_objects.ensure_routed_group(doc, "project-1") wire = doc.addObject("Part::Feature", "QETRoutedConnection_main_path") wire.Label = "N-MAINPATH: A1 -> B1" wire.RouteType = "RoutedConnection" wire.QetWireUuid = "wire-main-path" wire.PropertiesList = [ "QetStartTerminalUuid", "QetEndTerminalUuid", "QetRouteDiagnosticsJson", ] wire.QetStartTerminalUuid = "terminal-a" wire.QetEndTerminalUuid = "terminal-b" wire.QetRouteIssueCodes = "collision_warnings, main_path_detour_missing" wire.QetRouteDiagnosticsJson = json.dumps( { "selective_collision_reroute": { "status": "RejectedFallback", "rejected_fallback_kinds": ["RoutingRange"], "rejected_fallback_labels": ["安装板布线面"], } }, ensure_ascii=False, ) routed_group.addObject(wire) summary = auto_routing.collect_routing_diagnostic_summary(doc) message = auto_routing.format_routing_diagnostic_summary(summary) self.assertIn( "点击“选择缺主路径导线”定位需要补 UserPath 或主路径桥接的导线", summary["recommended_actions"], ) self.assertIn( "选中缺主路径导线后点击“选择拒绝兜底路径”查看需补路径位置", summary["recommended_actions"], ) self.assertIn( "点击“选择缺主路径补路位置”快速定位汇总需补区域", summary["recommended_actions"], ) self.assertIn( "点击“选择缺主路径线路径”对照当前实际路径", summary["recommended_actions"], ) self.assertIn("点击“选择缺主路径导线”", message) self.assertIn("选择缺主路径补路位置", message) self.assertIn("选择缺主路径线路径", message) self.assertIn("选择拒绝兜底路径", message) def test_routing_preflight_reports_no_routing_sources_when_network_is_empty(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)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertIn("no_routing_sources", report["issue_codes"]) self.assertEqual(0, report["routing_sources"]["candidate_sources"]) self.assertIn("布线源:未识别到线槽/布线面/用户路径", message) self.assertEqual(0, report["routing_boundaries"]["count"]) self.assertIn("柜内边界:未标记", message) def test_routing_preflight_reports_cabinet_boundary_count(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)) boundary = doc.addObject("Part::Feature", "CabinetBoundary") boundary.Label = "柜内空间" boundary.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 500)) routing_network.mark_cabinet_interior_boundaries_from_selection( [FakeSelectionItem(obj=boundary)] ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertEqual(1, report["routing_boundaries"]["count"]) self.assertEqual("柜内空间", report["routing_boundaries"]["samples"][0]["label"]) self.assertIn("柜内边界:1 个", message) def test_routing_preflight_reports_path_network_boundary_issues(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, "TerminalInside", "terminal-inside", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) boundary = doc.addObject("Part::Feature", "CabinetBoundary") boundary.Label = "柜内空间" boundary.Shape = FakeShape(FakeBoundBox(-20, 120, -20, 20, -10, 80)) routing_network.mark_cabinet_interior_boundaries_from_selection( [FakeSelectionItem(obj=boundary)] ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="柜内主路径A", project_uuid="project-1", kind="UserPath", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-outside-terminal", "wire_label": "N-OUT-TERM", "start_terminal_uuid": "terminal-inside", "end_terminal_uuid": "terminal-outside", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertFalse(report["ok"]) self.assertIn("terminals_outside_boundary", report["issue_codes"]) self.assertIn( "terminals_outside_boundary", report["routing_path_network_diagnostic"]["issue_codes"], ) self.assertEqual( "terminal-outside", report["routing_path_network_diagnostic"]["terminals_outside_boundary"][0]["terminal_uuid"], ) self.assertIn("路径网络检查提示", message) self.assertIn("端子越出柜内边界", message) self.assertIn("越界端子:TerminalOutside(terminal-outside) 2 个越界点", message) def test_check_routing_path_network_warns_when_route_carrier_leaves_cabinet_boundary(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, "TerminalStart", "terminal-start", app.Vector(0, 0, 0)) _terminal(doc, terminal_objects, "TerminalEnd", "terminal-end", app.Vector(100, 0, 0)) boundary = doc.addObject("Part::Feature", "CabinetBoundary") boundary.Label = "柜内空间" boundary.Shape = FakeShape(FakeBoundBox(0, 120, -20, 20, 0, 80)) routing_network.mark_cabinet_interior_boundaries_from_selection( [FakeSelectionItem(obj=boundary)] ) route = routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20), app.Vector(140, 0, 20)], label="柜内主路径A", project_uuid="project-1", kind="UserPath", ) 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.assertIn("route_carriers_outside_boundary", result["issue_codes"]) self.assertEqual(1, len(payload["route_carriers_outside_boundary"])) self.assertEqual(route.Name, payload["route_carriers_outside_boundary"][0]["carrier"]["name"]) self.assertEqual(1, payload["route_carriers_outside_boundary"][0]["outside_point_count"]) self.assertIn("路径越出柜内边界", message) self.assertIn("柜内主路径A", message) self.assertEqual((1.0, 0.0, 0.0), route.ViewObject.LineColor) def test_check_routing_path_network_warns_when_terminal_leaves_cabinet_boundary(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") inside = _terminal(doc, terminal_objects, "TerminalInside", "terminal-inside", app.Vector(0, 0, 0)) outside = _terminal(doc, terminal_objects, "TerminalOutside", "terminal-outside", app.Vector(140, 0, 0)) boundary = doc.addObject("Part::Feature", "CabinetBoundary") boundary.Label = "柜内空间" boundary.Shape = FakeShape(FakeBoundBox(-20, 120, -20, 20, -10, 80)) routing_network.mark_cabinet_interior_boundaries_from_selection( [FakeSelectionItem(obj=boundary)] ) routing_network.create_route_carrier( doc, [app.Vector(0, 0, 20), app.Vector(100, 0, 20)], label="柜内主路径A", project_uuid="project-1", kind="UserPath", ) 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.assertIn("terminals_outside_boundary", result["issue_codes"]) self.assertEqual(1, len(payload["terminals_outside_boundary"])) self.assertEqual("terminal-outside", payload["terminals_outside_boundary"][0]["terminal_uuid"]) self.assertIn("端子越出柜内边界", message) self.assertIn("terminal-outside", message) self.assertEqual((1.0, 0.0, 0.0), outside.ViewObject.LineColor) self.assertNotEqual((1.0, 0.0, 0.0), inside.ViewObject.LineColor) def test_routing_preflight_reports_detected_sources_not_generated(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)) panel = doc.addObject("Part::Feature", "MountingPlate") panel.Label = "安装板" panel.Shape = FakeShape(FakeBoundBox(0, 300, 0, 200, 0, 5)) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertIn("routing_sources_not_generated", report["issue_codes"]) self.assertEqual(1, report["routing_sources"]["support_surface_sources"]) self.assertEqual(1, report["routing_sources"]["candidate_sources"]) self.assertIn("布线源:线槽 0 个,布线面 1 个", message) self.assertIn("请先生成布线路径网络", message) def test_routing_preflight_reports_unrouteable_wire_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)) _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", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-far", "wire_label": "N-FAR", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections( doc, payload, options={ "terminal_access_max_distance": 50.0, "preflight_routeability_sample_limit": 1, }, ) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertFalse(report["ok"]) self.assertEqual(1, report["routeability_checked"]) self.assertEqual(1, report["unrouteable_wires"]) self.assertEqual("wire-far", report["unrouteable_samples"][0]["wire_uuid"]) self.assertIn("unrouteable_wires", report["issue_codes"]) self.assertIn("导线不可达", message) def test_routing_preflight_checks_routeability_for_complete_wires_when_other_wires_miss_endpoints(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(5000, 0, 20), app.Vector(5100, 0, 20)], project_uuid="project-1", kind="WireDuct", ) payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-missing", "wire_label": "N-MISS", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-missing", }, { "wire_id": "wire-far", "wire_label": "N-FAR", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } report = auto_routing.preflight_eplan_connections( doc, payload, options={ "terminal_access_max_distance": 50.0, "preflight_routeability_sample_limit": 1, }, ) self.assertFalse(report["ok"]) self.assertEqual(["terminal-missing"], report["missing_endpoint_uuids"]) self.assertEqual(1, report["routeability_checked"]) self.assertEqual(1, report["unrouteable_wires"]) self.assertIn("missing_endpoints", report["issue_codes"]) self.assertIn("unrouteable_wires", report["issue_codes"]) def test_routing_preflight_disables_routeability_sampling_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)) 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", "end_terminal_uuid": "terminal-end", }, ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertEqual(0, report["routeability_sample_limit"]) self.assertEqual(0, report["routeability_checked"]) self.assertNotIn("可达性抽样", message) def test_routing_preflight_report_shows_routeability_sample_coverage(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", ) 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.preflight_eplan_connections( doc, payload, options={"preflight_routeability_sample_limit": 1}, ) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertEqual(1, report["routeability_checked"]) self.assertEqual(1, report["routeability_unchecked_wires"]) self.assertIn("可达性抽样:已检查 1 条,未检查 1 条", message) def test_routing_preflight_checks_wire_style_ids_before_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") _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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.db" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT, name TEXT, line_color TEXT ) """ ) connection.execute( "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", (7, "project-1", "绿色控制线", "#00ff00"), ) connection.commit() finally: connection.close() payload = { "project_uuid": "project-1", "wire_style_database_path": str(db_path), "wires": [ { "wire_id": "wire-1", "wire_label": "N1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "7", }, { "wire_id": "wire-2", "wire_label": "N2", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "404", }, { "wire_id": "wire-3", "wire_label": "N3", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", }, ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertFalse(report["ok"]) self.assertEqual("Available", report["wire_style_database"]["status"]) self.assertEqual(1, report["wire_style"]["resolved"]) self.assertEqual(1, report["wire_style"]["missing"]) self.assertEqual(1, report["wire_style"]["without_style_id"]) self.assertEqual("404", report["wire_style"]["missing_samples"][0]["wire_style_id"]) self.assertIn("missing_wire_styles", report["issue_codes"]) self.assertIn("wires_without_style_id", report["issue_codes"]) self.assertIn("导线样式库:可用", message) self.assertIn("已解析 1 条", message) self.assertIn("缺失样式 1 条", message) self.assertIn("未设置样式 1 条", message) def test_routing_preflight_reports_empty_wire_style_database(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", ) with tempfile.TemporaryDirectory() as temp_dir: db_path = Path(temp_dir) / "project-local.db" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT, name TEXT, line_color TEXT ) """ ) connection.commit() finally: connection.close() payload = { "project_uuid": "project-1", "wire_style_database_path": str(db_path), "wires": [ { "wire_id": "wire-1", "wire_label": "N1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "1", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertFalse(report["ok"]) self.assertEqual("EmptyWirePropertiesTable", report["wire_style_database"]["status"]) self.assertEqual(0, report["wire_style_database"]["wire_properties_count"]) self.assertIn("wire_style_database_empty", report["issue_codes"]) self.assertIn("导线样式库:wire_properties 为空", message) def test_routing_preflight_uses_matching_fallback_style_database_when_payload_database_is_empty(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", ) with tempfile.TemporaryDirectory() as temp_dir: wrong_dir = Path(temp_dir) / "wrong" / "datafiles" right_dir = Path(temp_dir) / "right" / "datafiles" exchange_dir = Path(temp_dir) / "right" / ".qet_freecad" wrong_dir.mkdir(parents=True) right_dir.mkdir(parents=True) exchange_dir.mkdir(parents=True) wrong_db = wrong_dir / "project-local.db" right_db = right_dir / "project-local.db" for db_path, rows in ( (wrong_db, []), (right_db, [(1, "project-1", "红色动力线", "#ff0000")]), ): connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT, name TEXT, line_color TEXT ) """ ) connection.executemany( "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", rows, ) connection.commit() finally: connection.close() json_path = exchange_dir / "2d_to_3d.json" json_path.write_text( json.dumps({"project_uuid": "project-1", "wires": []}), encoding="utf-8", ) app._qet_exchange_summary = {"json_path": str(json_path)} payload = { "project_uuid": "project-1", "wire_style_database_path": str(wrong_db), "wires": [ { "wire_id": "wire-1", "wire_label": "N1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "1", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertTrue(report["ok"]) self.assertEqual(str(right_db), report["wire_style_database"]["path"]) self.assertEqual(str(wrong_db), report["wire_style_database_fallback_from"]) self.assertEqual(1, report["wire_style"]["resolved"]) self.assertNotIn("wire_style_database_empty", report["issue_codes"]) self.assertIn("从备用库", message) def test_routing_preflight_does_not_backfill_styles_from_other_project_context(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-current") _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-current", kind="WireDuct", ) with tempfile.TemporaryDirectory() as temp_dir: json_path = Path(temp_dir) / "2d_to_3d.json" json_path.write_text( json.dumps( { "project_uuid": "project-old", "wires": [ { "wire_id": "wire-old", "wire_label": "OLD", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "99", } ], } ), encoding="utf-8", ) app._qet_exchange_summary = {"json_path": str(json_path)} payload = { "project_uuid": "project-current", "wires": [ { "wire_id": "wire-current", "wire_label": "CURRENT", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) message = auto_routing.format_eplan_routing_preflight_report(report) self.assertEqual("project-current", report["project_uuid"]) self.assertEqual(1, report["total_wires"]) self.assertEqual(0, report["wire_style"]["with_style_id"]) self.assertNotIn("OLD", message) self.assertNotIn("99", message) def test_routing_preflight_discovers_style_database_from_exchange_summary_json_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") _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", ) with tempfile.TemporaryDirectory() as temp_dir: project_dir = Path(temp_dir) / "project-a" exchange_dir = project_dir / ".qet_freecad" data_dir = project_dir / "datafiles" exchange_dir.mkdir(parents=True) data_dir.mkdir(parents=True) json_path = exchange_dir / "2d_to_3d.json" json_path.write_text( json.dumps( { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_label": "N1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", "wire_style_id": "1", } ], } ), encoding="utf-8", ) db_path = data_dir / "project-local.db" connection = sqlite3.connect(str(db_path)) try: connection.execute( """ CREATE TABLE wire_properties ( id INTEGER PRIMARY KEY, project_uuid TEXT, name TEXT, line_color TEXT ) """ ) connection.execute( "INSERT INTO wire_properties (id, project_uuid, name, line_color) VALUES (?, ?, ?, ?)", (1, "project-1", "红色动力线", "#ff0000"), ) connection.commit() finally: connection.close() app._qet_exchange_summary = {"json_path": str(json_path)} payload = { "project_uuid": "project-1", "wires": [ { "wire_id": "wire-1", "wire_label": "N1", "start_terminal_uuid": "terminal-start", "end_terminal_uuid": "terminal-end", } ], } report = auto_routing.preflight_eplan_connections(doc, payload) self.assertTrue(report["ok"]) self.assertEqual(str(db_path), report["wire_style_database"]["path"]) self.assertEqual("Available", report["wire_style_database"]["status"]) self.assertEqual(1, report["wire_style"]["resolved"]) 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()