import importlib 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 getattr(vector, "z", None) == 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 selection_state = {"selection": [], "selection_ex": []} 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: list(selection_state["selection"]), getSelectionEx=lambda: list(selection_state["selection_ex"]), ) fake_freecadgui.Control = types.SimpleNamespace( activeDialog=lambda: False, showDialog=lambda panel: panel, closeDialog=lambda: None, ) 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 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) return obj def make_point(X=0, Y=0, Z=0, color=None, name="Point", point_size=5): doc = fake_freecad.ActiveDocument obj = doc.addObject("Part::FeaturePython", name) if isinstance(X, fake_freecad.Vector): point = X else: point = fake_freecad.Vector(X, Y, Z) obj.Point = point obj.Placement = fake_freecad.Placement(point, fake_freecad.Rotation()) obj.PointColor = color obj.PointSize = point_size return obj fake_draft.make_wire = make_wire fake_draft.make_point = make_point sys.modules["Draft"] = fake_draft return selection_state 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", "TemplateAuthoring", "ExchangeWriteBack", "ManualWiringPanel", ]: sys.modules.pop(name, None) terminal_objects = importlib.import_module("TerminalObjects") panel = importlib.import_module("ManualWiringPanel") return terminal_objects, panel class ManualWiringPanelTest(unittest.TestCase): def test_controller_creates_preview_point_and_records_face_anchor(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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") 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)), ) device.addObject(start_terminal) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-a", "terminal-start", "instance-a", label="Start", ) controller = panel.ManualWiringController(terminal_exit_length=10.0) selection_state["selection"] = [start_terminal] controller.set_start_from_selection() face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(1, 0, 0), ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(10, 20, 30)], SubObjects=[face], SubElementNames=["Face1"], Object=types.SimpleNamespace(Name="CabinetFace", Label="柜体面"), ) ] waypoint = controller.add_waypoint_from_selection() preview_group = doc.getObject("QETWiring_03_Previews") self.assertIsNotNone(preview_group) self.assertEqual(1, len(controller.waypoints)) self.assertEqual("face", waypoint["anchor_kind"]) self.assertEqual("x", waypoint["support_axis"]) self.assertEqual(1, len(controller.preview_objects)) self.assertIn(controller.preview_objects[0], preview_group.Group) self.assertEqual( (10.0, 20.0, 30.0), ( controller.preview_objects[0].Point.x, controller.preview_objects[0].Point.y, controller.preview_objects[0].Point.z, ), ) def test_controller_generates_direct_wire_from_waypoint_and_end_selection(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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") 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)), ) device.addObject(start_terminal) terminal_objects.set_terminal_semantics( start_terminal, "project-1", "device-a", "terminal-start", "instance-a", 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)), ) device.addObject(end_terminal) terminal_objects.set_terminal_semantics( end_terminal, "project-1", "device-a", "terminal-end", "instance-a", label="End", ) controller = panel.ManualWiringController(terminal_exit_length=10.0) selection_state["selection"] = [start_terminal] controller.set_start_from_selection() selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(10, 20, 30)], SubObjects=[ types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(1, 0, 0), ) ], SubElementNames=["Face1"], Object=types.SimpleNamespace(Name="CabinetFace", Label="柜体面"), ) ] controller.add_waypoint_from_selection() selection_state["selection"] = [end_terminal] wire = controller.set_end_from_selection_and_generate() routed_group = doc.getObject("QETWiring_04_Routed") self.assertIsNotNone(routed_group) self.assertIn(wire, routed_group.Group) self.assertEqual("terminal-start", getattr(wire, "QetStartTerminalUuid", "")) self.assertEqual("terminal-end", getattr(wire, "QetEndTerminalUuid", "")) self.assertEqual(5, len(getattr(wire, "Points", []))) self.assertEqual( (1.0, 12.0, 3.0), ( wire.Points[1].x, wire.Points[1].y, wire.Points[1].z, ), ) self.assertEqual((10.0, 20.0, 30.0), (wire.Points[2].x, wire.Points[2].y, wire.Points[2].z)) self.assertEqual((9.0, 8.0, 17.0), (wire.Points[3].x, wire.Points[3].y, wire.Points[3].z)) self.assertEqual((9.0, 8.0, 7.0), (wire.Points[4].x, wire.Points[4].y, wire.Points[4].z)) if __name__ == "__main__": unittest.main()