import sys import types import unittest import json 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 __init__(self, axis=None, angle=None): self.Axis = axis self.Angle = angle class Placement: def __init__(self, base=None, rotation=None): self.Base = base or Vector() self.Rotation = 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.Console = types.SimpleNamespace( PrintMessage=lambda *args, **kwargs: None, PrintWarning=lambda *args, **kwargs: None, PrintError=lambda *args, **kwargs: None, ) fake_freecad.ActiveDocument = None sys.modules["FreeCAD"] = fake_freecad fake_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None fake_freecadgui.addCommand = lambda *args, **kwargs: None fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: []) sys.modules["FreeCADGui"] = fake_freecadgui fake_importgui = types.ModuleType("ImportGui") fake_importgui.insert = lambda *args, **kwargs: None sys.modules["ImportGui"] = fake_importgui 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 class FakeObject: def __init__(self, name, type_id): self.Name = name self.Label = name self.TypeId = type_id self.PropertiesList = [] self.Group = [] self.ViewObject = FakeViewObject() self.Shape = None 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) 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 def _reload_modules(): for name in [ "TerminalObjects", "DeviceImport", "ManualWiring", ]: sys.modules.pop(name, None) import DeviceImport import ManualWiring import TerminalObjects return DeviceImport, ManualWiring, TerminalObjects class ManualWiringGroupTest(unittest.TestCase): def test_manual_wire_uses_terminal_global_points_after_device_move(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") start_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_start") start_device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) root.addObject(start_device) end_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_end") end_device.Placement = app.Placement(app.Vector(300, 0, 0), app.Rotation()) root.addObject(end_device) for device, element_uuid, instance_id in [ (start_device, "device-start", "instance-start"), (end_device, "device-end", "instance-end"), ]: terminal_objects.ensure_string_property( device, "QetElementUuid", "QET Exchange", "Element UUID", element_uuid, ) terminal_objects.ensure_string_property( device, "QetInstanceId", "QET Exchange", "Instance ID", instance_id, ) start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation()) start_device.addObject(start_terminal) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) end_device.addObject(end_terminal) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire(doc, start_terminal, end_terminal) self.assertEqual(110.0, wire.Shape[0].x) self.assertEqual(320.0, wire.Shape[-1].x) def test_manual_wire_routes_orthogonally_between_terminal_exit_points(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(10, 20, 30), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, terminal_exit_length=10.0, ) points = [(point.x, point.y, point.z) for point in wire.Shape] self.assertEqual( [ (0.0, 0.0, 0.0), (0.0, 0.0, 10.0), (0.0, 0.0, 40.0), (0.0, 20.0, 40.0), (10.0, 20.0, 40.0), (10.0, 20.0, 30.0), ], points, ) def test_manual_wire_reaches_face_anchor_normal_axis_last(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(40, 20, -100), "support_axis": "z", "anchor_kind": "face", } ], terminal_exit_length=20.0, ) points = [(point.x, point.y, point.z) for point in wire.Shape] self.assertEqual( [ (0.0, 0.0, 0.0), (0.0, 0.0, 20.0), (40.0, 0.0, 20.0), (40.0, 20.0, 20.0), (40.0, 20.0, -100.0), ], points[:5], ) def test_manual_wire_records_semantic_route_nodes_for_later_carrier_routing(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(100, 20, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(50, 10, 30), "support_axis": "z", "anchor_kind": "edge", "carrier_kind": "wire_duct", "source_label": "线槽A", "subelement_name": "Edge1", } ], terminal_exit_length=15.0, ) self.assertEqual(15.0, getattr(wire, "QetTerminalExitLength", None)) route_nodes = json.loads(getattr(wire, "QetRouteNodesJson", "[]")) self.assertEqual( [ "start_terminal", "start_exit", "waypoint", "end_exit", "end_terminal", ], [node["role"] for node in route_nodes], ) self.assertEqual("wire_duct", route_nodes[2]["carrier_kind"]) self.assertEqual("edge", route_nodes[2]["anchor_kind"]) self.assertEqual("terminal-start", route_nodes[0]["terminal_uuid"]) self.assertEqual("terminal-end", route_nodes[-1]["terminal_uuid"]) def test_manual_wire_routes_along_same_wire_duct_axis_between_waypoints(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(30, 120, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(10, 0, 20), "carrier_kind": "wire_duct", "carrier_axis": "x", "source_object_name": "WireDuct_A", }, { "point": app.Vector(20, 100, 20), "carrier_kind": "wire_duct", "carrier_axis": "x", "source_object_name": "WireDuct_A", }, ], terminal_exit_length=0.0, ) points = [(point.x, point.y, point.z) for point in wire.Shape] self.assertEqual( [ (10.0, 0.0, 20.0), (20.0, 0.0, 20.0), (20.0, 100.0, 20.0), ], points[2:5], ) route_nodes = json.loads(getattr(wire, "QetRouteNodesJson", "[]")) self.assertEqual("x", route_nodes[1]["carrier_axis"]) self.assertEqual("x", route_nodes[2]["carrier_axis"]) def test_manual_wire_does_not_treat_unknown_wire_duct_sources_as_same_carrier(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(30, 120, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(10, 0, 20), "carrier_kind": "wire_duct", "carrier_axis": "x", }, { "point": app.Vector(20, 100, 20), "carrier_kind": "wire_duct", "carrier_axis": "x", }, ], terminal_exit_length=0.0, ) points = [(point.x, point.y, point.z) for point in wire.Shape] self.assertEqual((10.0, 100.0, 20.0), points[3]) def test_manual_wire_diagnostics_warn_for_wire_duct_waypoint_without_source(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(10, 10, 10), "carrier_kind": "wire_duct", "carrier_axis": "x", } ], ) diagnostics = manual_wiring.diagnose_manual_wire(wire) self.assertTrue( any(item["code"] == "wire_duct_source_missing" for item in diagnostics) ) def test_manual_wire_diagnostics_pass_for_complete_manual_route(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(10, 10, 10), "carrier_kind": "wire_duct", "carrier_axis": "x", "source_object_name": "WireDuct_A", } ], terminal_exit_length=20.0, ) self.assertEqual([], manual_wiring.diagnose_manual_wire(wire)) def test_write_document_wire_diagnostics_creates_diagnostic_objects(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) wire = manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(10, 10, 10), "carrier_kind": "wire_duct", "carrier_axis": "x", } ], ) report = manual_wiring.write_document_wire_diagnostics(doc) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") self.assertEqual(1, report["issue_count"]) self.assertEqual(1, len(diagnostic_group.Group)) diagnostic = diagnostic_group.Group[0] self.assertEqual("wire_duct_source_missing", diagnostic.QetDiagnosticCode) self.assertEqual(wire.Name, diagnostic.QetWireObjectName) self.assertIn("线槽折点", diagnostic.QetDiagnosticMessage) def test_write_document_wire_diagnostics_replaces_previous_manual_results(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() terminal_objects.ensure_root_group(doc, "project-1") start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-start", "terminal-start", "instance-start", label="Start", ) end_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalEnd") end_terminal.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-end", "terminal-end", "instance-end", label="End", ) manual_wiring.create_manual_wire( doc, start_terminal, end_terminal, waypoints=[ { "point": app.Vector(10, 10, 10), "carrier_kind": "wire_duct", "carrier_axis": "x", } ], ) manual_wiring.write_document_wire_diagnostics(doc) manual_wiring.write_document_wire_diagnostics(doc) diagnostic_group = doc.getObject("QETWiring_05_Diagnostics") self.assertEqual(1, len(diagnostic_group.Group)) def test_manual_wire_is_visible_in_routed_group_not_hidden_legacy_group(self): _install_fake_freecad() _device_import, manual_wiring, terminal_objects = _reload_modules() wiring_objects = __import__("WiringObjects") doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") device_group = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") root.addObject(device_group) terminal_objects.ensure_string_property( device_group, "QetElementUuid", "QET Exchange", "Element UUID", "device-a", ) terminal_objects.ensure_string_property( device_group, "QetInstanceId", "QET Exchange", "Instance ID", "instance-a", ) terminal_objects.ensure_string_property( device_group, "QetProjectUuid", "QET Exchange", "Project UUID", "project-1", ) start_terminal = FakeObject("TerminalStart", "Part::LocalCoordinateSystem") terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-a", "terminal-start", "instance-a", label="Start", ) end_terminal = FakeObject("TerminalEnd", "Part::LocalCoordinateSystem") end_terminal.Placement = sys.modules["FreeCAD"].Placement( sys.modules["FreeCAD"].Vector(10, 0, 0), sys.modules["FreeCAD"].Rotation(), ) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-a", "terminal-end", "instance-a", label="End", ) wire = manual_wiring.create_manual_wire(doc, start_terminal, end_terminal) wire_group = terminal_objects.find_child_group_by_kind( device_group, terminal_objects.WIRE_GROUP_KIND, ) routed_group = wiring_objects.ensure_routed_group(doc, "project-1") self.assertIsNotNone(wire_group) self.assertFalse(wire_group.ViewObject.Visibility) self.assertIn(wire, routed_group.Group) self.assertTrue(wire.ViewObject.Visibility) self.assertNotIn(wire, wire_group.Group) self.assertNotIn(wire, root.Group) if __name__ == "__main__": unittest.main()