import sys import types import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] MODULE_DIR = REPO_ROOT / "src" / "Mod" / "FreeCADExchange" if str(MODULE_DIR) not in sys.path: sys.path.insert(0, str(MODULE_DIR)) def _install_fake_freecad(): class Vector: def __init__(self, x=0.0, y=0.0, z=0.0): self.x = float(x) self.y = float(y) self.z = float(z) class Rotation: def __init__(self, axis=None, angle=None, w_axis=None): self.Axis = axis self.Angle = angle self.WAxis = w_axis def multVec(self, vector): if self.WAxis is not None and vector.z == 1: return self.WAxis 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.GuiUp = True fake_freecad.Console = types.SimpleNamespace( PrintMessage=lambda *args, **kwargs: None, PrintWarning=lambda *args, **kwargs: None, PrintError=lambda *args, **kwargs: None, PrintLog=lambda *args, **kwargs: None, ) sys.modules["FreeCAD"] = fake_freecad fake_freecadgui = types.ModuleType("FreeCADGui") fake_freecadgui.addCommand = lambda *args, **kwargs: None fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None fake_freecadgui.Selection = types.SimpleNamespace( getSelection=lambda: [], getSelectionEx=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 FakeDraftWire: def __init__(self, obj): obj.addProperty("App::PropertyVectorList", "Points", "Draft", "Wire points") fake_draft = types.ModuleType("Draft") def make_wire(points, closed=False, placement=None, face=None, support=None, bs2wire=False): doc = fake_freecad.ActiveDocument obj = doc.addObject("Part::FeaturePython", "Wire") obj.Points = list(points) obj.Closed = bool(closed) obj.AttachmentSupport = support obj.Placement = placement or fake_freecad.Placement() FakeDraftWire(obj) return obj fake_draft.make_wire = make_wire sys.modules["Draft"] = fake_draft 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.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) 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", "WiringObjects", "ManualWiring", "ExchangeWriteBack"]: sys.modules.pop(name, None) import TerminalObjects import WiringObjects import ManualWiring import ExchangeWriteBack return TerminalObjects, WiringObjects, ManualWiring, ExchangeWriteBack class WiringTest(unittest.TestCase): def test_ensure_wiring_root_group_creates_scene_buckets(self): _install_fake_freecad() terminal_objects, wiring_objects, _manual_wiring, _write_back = _reload_modules() doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") wiring_root = wiring_objects.ensure_wiring_root_group(doc, "project-1") self.assertIn(wiring_root, root.Group) self.assertEqual("QETWiring", wiring_root.Name) self.assertEqual("QET Wiring", wiring_root.Label) self.assertIsNotNone(doc.getObject("QETWiring_01_Tasks")) self.assertIsNotNone(doc.getObject("QETWiring_04_Routed")) def test_initialize_wiring_scene_creates_root_and_hides_legacy_wire_groups(self): _install_fake_freecad() terminal_objects, wiring_objects, _manual_wiring, _write_back = _reload_modules() 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") legacy_group = terminal_objects.ensure_wire_group( doc, device, project_uuid="project-1", instance_id="instance-a", ) legacy_group.ViewObject.Visibility = True wiring_root = wiring_objects.initialize_wiring_scene(doc, "project-1") self.assertEqual("QETWiring", wiring_root.Name) self.assertIsNotNone(doc.getObject("QETWiring_04_Routed")) self.assertFalse(legacy_group.ViewObject.Visibility) def test_create_manual_wire_preserves_manual_waypoints_as_orthogonal_segments(self): _install_fake_freecad() terminal_objects, wiring_objects, manual_wiring, _write_back = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc root = terminal_objects.ensure_root_group(doc, "project-1") wiring_objects.ensure_wiring_root_group(doc, "project-1") start_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_start") root.addObject(start_device) terminal_objects.ensure_string_property( start_device, "QetElementUuid", "QET Exchange", "", "device-start", ) terminal_objects.ensure_string_property( start_device, "QetInstanceId", "QET Exchange", "", "instance-start", ) terminal_objects.ensure_string_property( start_device, "QetProjectUuid", "QET Exchange", "", "project-1", ) end_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_end") root.addObject(end_device) terminal_objects.ensure_string_property( end_device, "QetElementUuid", "QET Exchange", "", "device-end", ) terminal_objects.ensure_string_property( end_device, "QetInstanceId", "QET Exchange", "", "instance-end", ) terminal_objects.ensure_string_property( end_device, "QetProjectUuid", "QET Exchange", "", "project-1", ) start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart") start_terminal.Placement = app.Placement( app.Vector(1, 2, 3), app.Rotation(w_axis=app.Vector(0, 1, 0)), ) 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(9, 8, 7), app.Rotation(w_axis=app.Vector(0, 0, 1)), ) 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, waypoints=[ { "point": app.Vector(4, 5, 6), "support_axis": "x", "anchor_kind": "face", "source_label": "柜体面", } ], terminal_exit_length=20.0, ) routed_group = doc.getObject("QETWiring_04_Routed") self.assertIsNotNone(routed_group) self.assertIn(wire, routed_group.Group) self.assertEqual("Manual", getattr(wire, "RouteType", "")) self.assertEqual("terminal-start", getattr(wire, "QetStartTerminalUuid", "")) self.assertEqual("terminal-end", getattr(wire, "QetEndTerminalUuid", "")) self.assertEqual( [ (1.0, 2.0, 3.0), (1.0, 22.0, 3.0), (1.0, 5.0, 3.0), (1.0, 5.0, 6.0), (4.0, 5.0, 6.0), (4.0, 5.0, 27.0), (9.0, 5.0, 27.0), (9.0, 8.0, 27.0), (9.0, 8.0, 7.0), ], [(point.x, point.y, point.z) for point in getattr(wire, "Points", [])], ) self.assertTrue(any(point.x == 4.0 and point.y == 5.0 and point.z == 6.0 for point in wire.Points)) self.assertIn("QetManualWaypointsJson", getattr(wire, "PropertiesList", [])) self.assertIn('"support_axis": "x"', getattr(wire, "QetManualWaypointsJson", "")) payload = wiring_objects.wire_payload_from_object(wire) self.assertEqual(20.0, payload["terminal_exit_length"]) self.assertEqual("Manual", payload["route_mode"]) self.assertEqual( ["start_terminal", "start_exit", "waypoint", "end_exit", "end_terminal"], [node["role"] for node in payload["route_nodes"]], ) self.assertEqual("face", payload["route_nodes"][2]["anchor_kind"]) def test_wire_writeback_omits_scene_routed_wire_payload(self): _install_fake_freecad() terminal_objects, wiring_objects, manual_wiring, write_back = _reload_modules() doc = FakeDocument() root = terminal_objects.ensure_root_group(doc, "project-1") wiring_objects.ensure_wiring_root_group(doc, "project-1") routed_group = doc.getObject("QETWiring_04_Routed") wire = doc.addObject("Part::Feature", "QETWire_terminal_start_terminal_end") wire.Shape = [ sys.modules["FreeCAD"].Vector(1, 2, 3), sys.modules["FreeCAD"].Vector(4, 5, 6), ] terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1") terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "terminal-start") terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-end") terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-start") terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-end") terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual") routed_group.addObject(wire) report = write_back.write_back_document(doc, scene_path=r"D:\tmp\scene.FCStd", payload={"project_uuid": "project-1"}) self.assertNotIn("manual_wires", report) if __name__ == "__main__": unittest.main()