import importlib import json 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 __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 def clear_selection(): selection_state["selection"] = [] def add_selection(obj): selection_state["selection"] = [obj] fake_freecadgui.Selection = types.SimpleNamespace( getSelection=lambda: list(selection_state["selection"]), getSelectionEx=lambda: list(selection_state["selection_ex"]), clearSelection=clear_selection, addSelection=add_selection, ) 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" self.transactions = [] 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 openTransaction(self, name): self.transactions.append(("open", name)) def commitTransaction(self): self.transactions.append(("commit", "")) def abortTransaction(self): self.transactions.append(("abort", "")) def undo(self): self.transactions.append(("undo", "")) def _reload_modules(): for name in [ "TerminalObjects", "WiringObjects", "ManualWiring", "TemplateAuthoring", "ExchangeWriteBack", "BatchAssembly", "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_builtin_carrier_asset_path_supports_installed_freecad_layout(self): _selection_state = _install_fake_freecad() _terminal_objects, panel = _reload_modules() with tempfile.TemporaryDirectory() as temp_dir: app_home = Path(temp_dir) / "run-FreeCAD" module_dir = app_home / "Mod" / "FreeCADExchange" asset_dir = app_home / "data" / "examples" / "qet_cabinet_assets" module_dir.mkdir(parents=True) asset_dir.mkdir(parents=True) asset = asset_dir / "qet_din_rail.FCStd" asset.write_text("fake rail", encoding="utf-8") original_file = panel.__file__ try: panel.__file__ = str(module_dir / "ManualWiringPanel.py") resolved = panel._builtin_carrier_asset_path("rail") finally: panel.__file__ = original_file self.assertEqual(str(asset), resolved) def test_controller_batch_creates_terminal_block_from_selected_rail(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(50, 0, 0), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") selection_state["selection"] = [rail] report = panel.ManualWiringController().create_terminal_block_from_selection( block_name="XT1", count=2, pitch_mm=5.2, ) self.assertEqual(2, report["created_devices"]) self.assertEqual(["XT1:1", "XT1:2"], [terminal.Label for terminal in report["terminals"]]) def test_controller_batch_breaker_default_creates_three_pole_terminal_numbers(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") selection_state["selection"] = [rail] report = panel.ManualWiringController().create_breakers_from_selection( base_name="QF", count=1, ) self.assertEqual(1, report["created_devices"]) self.assertEqual(["QF1:1", "QF1:2", "QF1:3", "QF1:4", "QF1:5", "QF1:6"], [terminal.Label for terminal in report["terminals"]]) def test_controller_batch_accepts_empty_model_path_from_dialog_options(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail") terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x") selection_state["selection"] = [rail] report = panel.ManualWiringController().create_breakers_from_selection( base_name="QF", count=1, model_path="", ) self.assertEqual(1, report["created_devices"]) def test_batch_terminal_block_options_validate_user_fields(self): _install_fake_freecad() _terminal_objects, panel = _reload_modules() options = panel._batch_terminal_block_options( block_name=" XT2 ", count=12, pitch_mm=6.2, start_offset_mm=-5, ) self.assertEqual( { "block_name": "XT2", "count": 12, "pitch_mm": 6.2, "start_offset_mm": -5.0, }, options, ) def test_batch_breaker_options_parse_terminal_number_text(self): _install_fake_freecad() _terminal_objects, panel = _reload_modules() options = panel._batch_breaker_options( base_name=" QF ", count=2, pitch_mm=18, start_offset_mm=0, terminal_numbers_text="1,2,3 4;5;6", ) self.assertEqual(("1", "2", "3", "4", "5", "6"), options["terminal_numbers"]) self.assertNotIn("model_path", options) def test_batch_breaker_options_reject_duplicate_terminal_numbers(self): _install_fake_freecad() _terminal_objects, panel = _reload_modules() with self.assertRaises(panel.ManualWiringPanelError): panel._batch_breaker_options( base_name="QF", count=1, pitch_mm=18, start_offset_mm=0, terminal_numbers_text="1,2,1", ) def test_controller_rejects_local_terminal_as_manual_wiring_start(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") local_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalLocal") terminal_objects.set_terminal_semantics( local_terminal, "project-1", "device-a", "local:instance-a:P1", "instance-a", label="P1", ) selection_state["selection"] = [local_terminal] with self.assertRaisesRegex(panel.ManualWiringPanelError, "QET 绑定工程端子"): panel.ManualWiringController().set_start_from_selection() 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_records_selected_wire_duct_waypoint_as_carrier_anchor(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") carrier = doc.addObject("Part::Feature", "WireDuct_A") carrier.Label = "线槽A" terminal_objects.ensure_string_property( carrier, "QetCarrierKind", "QET Wiring", "Carrier kind", "wire_duct", ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(100, 20, 30)], SubObjects=[ types.SimpleNamespace( ShapeType="Edge", normalAt=lambda u: app.Vector(0, 1, 0), ) ], SubElementNames=["Edge1"], Object=carrier, ) ] waypoint = panel.ManualWiringController().add_waypoint_from_selection() self.assertEqual("edge", waypoint["anchor_kind"]) self.assertEqual("wire_duct", waypoint["carrier_kind"]) self.assertEqual("线槽A", waypoint["source_label"]) self.assertEqual("WireDuct_A", waypoint["source_object_name"]) def test_controller_records_wire_duct_edge_axis_for_waypoint(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") carrier = doc.addObject("Part::Feature", "WireDuct_A") carrier.Label = "线槽A" terminal_objects.ensure_string_property( carrier, "QetCarrierKind", "QET Wiring", "Carrier kind", "wire_duct", ) edge = types.SimpleNamespace( ShapeType="Edge", Vertexes=[ types.SimpleNamespace(Point=app.Vector(0, 10, 20)), types.SimpleNamespace(Point=app.Vector(100, 10, 20)), ], ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(40, 10, 20)], SubObjects=[edge], SubElementNames=["Edge1"], Object=carrier, ) ] waypoint = panel.ManualWiringController().add_waypoint_from_selection() self.assertEqual("wire_duct", waypoint["carrier_kind"]) self.assertEqual("x", waypoint["carrier_axis"]) def test_controller_marks_selected_object_as_wire_duct_carrier(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") carrier = doc.addObject("Part::Feature", "WireDuct_A") carrier.Label = "线槽A" selection_state["selection"] = [carrier] marked = panel.ManualWiringController().mark_selected_carriers("wire_duct") carrier_group = doc.getObject("QETWiring_02_Carriers") self.assertEqual([carrier], marked) self.assertEqual("wire_duct", getattr(carrier, "QetCarrierKind", "")) self.assertEqual("线槽", getattr(carrier, "QetCarrierRoleLabel", "")) self.assertIn(carrier, carrier_group.Group) def test_controller_imports_wire_duct_carrier_from_asset_path(self): _selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") def importer(doc, path): obj = doc.addObject("Part::Feature", "ImportedWireDuct") obj.Label = "Imported Wire Duct" return [obj] carrier = panel.ManualWiringController().import_carrier_asset( r"D:\assets\duct.FCStd", "wire_duct", length_mm=600.0, importer=importer, ) carrier_group = doc.getObject("QETWiring_02_Carriers") self.assertEqual("wire_duct", getattr(carrier, "QetCarrierKind", "")) self.assertEqual("线槽", getattr(carrier, "QetCarrierRoleLabel", "")) self.assertEqual(r"D:\assets\duct.FCStd", getattr(carrier, "QetCarrierSourcePath", "")) self.assertEqual(200.0, getattr(carrier, "QetCarrierBaseLength", None)) self.assertEqual(600.0, getattr(carrier, "QetCarrierLength", None)) self.assertIn(carrier, carrier_group.Group) self.assertEqual(3.0, getattr(carrier, "QetCarrierScaleX", None)) def test_controller_restores_active_document_after_carrier_import_changes_it(self): _selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") def importer(doc, path): obj = doc.addObject("Part::Feature", "ImportedRail") app.ActiveDocument = None return [obj] panel.ManualWiringController().import_carrier_asset( r"D:\assets\rail.FCStd", "rail", length_mm=300.0, importer=importer, ) self.assertIs(doc, app.ActiveDocument) def test_controller_applies_length_to_selected_carrier(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") carrier = doc.addObject("App::DocumentObjectGroup", "Carrier") terminal_objects.ensure_string_property( carrier, "QetCarrierKind", "QET Wiring", "Carrier kind", "rail", ) carrier.addProperty("App::PropertyFloat", "QetCarrierBaseLength", "QET Wiring", "Base length") carrier.QetCarrierBaseLength = 200.0 selection_state["selection"] = [carrier] updated = panel.ManualWiringController().apply_length_to_selected_carriers(500.0) self.assertEqual([carrier], updated) self.assertEqual(500.0, getattr(carrier, "QetCarrierLength", None)) self.assertEqual(2.5, getattr(carrier, "QetCarrierScaleX", None)) def test_controller_applies_length_when_selected_object_is_carrier_child(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") carrier = doc.addObject("App::DocumentObjectGroup", "WireDuctCarrier") child = doc.addObject("Part::Feature", "WireDuctBody") nested_child = doc.addObject("Part::Feature", "WireDuctBodyFaceOwner") carrier.addObject(child) child.addObject(nested_child) terminal_objects.ensure_string_property( carrier, "QetCarrierKind", "QET Wiring", "Carrier kind", "wire_duct", ) carrier.addProperty("App::PropertyFloat", "QetCarrierBaseLength", "QET Wiring", "Base length") carrier.QetCarrierBaseLength = 200.0 selection_state["selection"] = [nested_child] updated = panel.ManualWiringController().apply_length_to_selected_carriers(400.0) self.assertEqual([carrier], updated) self.assertEqual(400.0, getattr(carrier, "QetCarrierLength", None)) self.assertEqual(2.0, getattr(carrier, "QetCarrierScaleX", None)) def test_controller_auto_marks_selected_wire_duct_by_name_before_length_change(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") duct = doc.addObject("Part::Feature", "qet_wire_duct_001") duct.Label = "线槽001" selection_state["selection"] = [duct] updated = panel.ManualWiringController().apply_length_to_selected_carriers(450.0) carrier_group = doc.getObject("QETWiring_02_Carriers") self.assertEqual([duct], updated) self.assertEqual("wire_duct", duct.QetCarrierKind) self.assertEqual("线槽", duct.QetCarrierRoleLabel) self.assertIn(duct, carrier_group.Group) self.assertEqual(450.0, duct.QetCarrierLength) def test_controller_auto_detects_selected_din_rail_for_batch_placement(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") rail = doc.addObject("App::DocumentObjectGroup", "DINRail001") rail.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) selection_state["selection"] = [rail] report = panel.ManualWiringController().create_breakers_from_selection( base_name="QF", count=1, ) self.assertEqual(1, report["created_devices"]) self.assertEqual("rail", rail.QetCarrierKind) def test_controller_aligns_second_selected_face_to_first_selected_face(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cabinet = doc.addObject("Part::Feature", "CabinetPanel") rail = doc.addObject("Part::Feature", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) target_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, 1), ) moving_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, -1), ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(100, 20, 0)], SubObjects=[target_face], SubElementNames=["Face1"], Object=cabinet, ), types.SimpleNamespace( PickedPoints=[app.Vector(5, 6, 9)], SubObjects=[moving_face], SubElementNames=["Face2"], Object=rail, ), ] result = panel.ManualWiringController().align_selected_contact_faces() self.assertIs(rail, result["moving_object"]) self.assertEqual( (0.0, 0.0, 1.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z), ) self.assertEqual( (-0.0, -0.0, -9.0), ( result["translation"].x, result["translation"].y, result["translation"].z, ), ) self.assertEqual("normal", result["translation_mode"]) def test_controller_records_face_contact_mount_host_after_alignment(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cabinet = doc.addObject("Part::Feature", "CabinetPanel") rail = doc.addObject("Part::Feature", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) terminal_objects.ensure_string_property( cabinet, "QetCarrierKind", "QET Wiring", "3D wiring carrier kind", "cabinet", ) terminal_objects.ensure_string_property( rail, "QetCarrierKind", "QET Wiring", "3D wiring carrier kind", "rail", ) target_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, 1), ) moving_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, -1), ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(100, 20, 0)], SubObjects=[target_face], SubElementNames=["Face1"], Object=cabinet, ), types.SimpleNamespace( PickedPoints=[app.Vector(5, 6, 9)], SubObjects=[moving_face], SubElementNames=["Face2"], Object=rail, ), ] result = panel.ManualWiringController().align_selected_contact_faces() self.assertIs(cabinet, result["target_object"]) self.assertEqual("face_contact", rail.QetMountMode) self.assertEqual("CabinetPanel", rail.QetMountHostName) self.assertEqual("CabinetPanel", rail.QetMountHostLabel) self.assertEqual("cabinet", rail.QetMountHostKind) self.assertEqual("rail", rail.QetMountKind) self.assertEqual("Face1", rail.QetMountHostSubElement) self.assertEqual("Face2", rail.QetMountContactSubElement) self.assertEqual({"x": 0.0, "y": 0.0, "z": 0.0}, json.loads(rail.QetMountHostBaseJson)) self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountLocalBaseJson)) def test_refresh_mount_hosted_objects_moves_child_by_host_delta(self): _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc cabinet = doc.addObject("Part::Feature", "CabinetPanel") rail = doc.addObject("Part::Feature", "DINRail") cabinet.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation()) rail.Placement = app.Placement(app.Vector(120, 0, 5), app.Rotation()) terminal_objects.ensure_string_property( rail, "QetMountMode", "QET Assembly", "QET cabinet assembly mount metadata", "face_contact", ) terminal_objects.ensure_string_property( rail, "QetMountHostName", "QET Assembly", "QET cabinet assembly mount metadata", "CabinetPanel", ) terminal_objects.ensure_string_property( rail, "QetMountLocalBaseJson", "QET Assembly", "QET cabinet assembly local base offset", json.dumps({"x": 20.0, "y": 0.0, "z": 5.0}, ensure_ascii=False), ) cabinet.Placement = app.Placement(app.Vector(130, 0, 0), app.Rotation()) updated = panel.refresh_mount_hosted_objects(doc) self.assertEqual([rail], updated) self.assertEqual((150.0, 0.0, 5.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) self.assertEqual({"x": 130.0, "y": 0.0, "z": 0.0}, json.loads(rail.QetMountHostBaseJson)) def test_controller_refreshes_mount_hosted_objects_from_active_document(self): _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc cabinet = doc.addObject("Part::Feature", "CabinetPanel") rail = doc.addObject("Part::Feature", "DINRail") cabinet.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation()) rail.Placement = app.Placement(app.Vector(15, 0, 0), app.Rotation()) terminal_objects.ensure_string_property( rail, "QetMountMode", "QET Assembly", "QET cabinet assembly mount metadata", "face_contact", ) terminal_objects.ensure_string_property( rail, "QetMountHostName", "QET Assembly", "QET cabinet assembly mount metadata", "CabinetPanel", ) terminal_objects.ensure_string_property( rail, "QetMountLocalBaseJson", "QET Assembly", "QET cabinet assembly local base offset", json.dumps({"x": 5.0, "y": 0.0, "z": 0.0}, ensure_ascii=False), ) cabinet.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation()) updated = panel.ManualWiringController().refresh_mount_hosts() self.assertEqual([rail], updated) self.assertEqual((25.0, 0.0, 0.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) def test_controller_uses_stored_target_face_for_single_moving_face_alignment(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cabinet = doc.addObject("Part::Feature", "CabinetPanel") rail = doc.addObject("Part::Feature", "DINRail") rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) target_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, 1), ) moving_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, -1), ) controller = panel.ManualWiringController() selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(100, 20, 0)], SubObjects=[target_face], SubElementNames=["Face1"], Object=cabinet, ) ] controller.set_contact_target_from_selection() selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(5, 6, 9)], SubObjects=[moving_face], SubElementNames=["Face2"], Object=rail, ) ] result = controller.align_selected_contact_faces() self.assertIs(rail, result["moving_object"]) self.assertEqual((0.0, 0.0, 1.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z)) def test_controller_uses_largest_object_face_when_no_subface_is_selected(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cabinet = doc.addObject("Part::Feature", "CabinetPanel") small_face = types.SimpleNamespace( ShapeType="Face", Area=10.0, CenterOfMass=app.Vector(0, 0, 5), normalAt=lambda u, v: app.Vector(0, 1, 0), ) large_face = types.SimpleNamespace( ShapeType="Face", Area=1000.0, CenterOfMass=app.Vector(0, 0, 0), normalAt=lambda u, v: app.Vector(0, 0, 1), ) cabinet.Shape = types.SimpleNamespace(Faces=[small_face, large_face]) selection_state["selection"] = [cabinet] selection_state["selection_ex"] = [] target = panel.ManualWiringController().set_contact_target_from_selection() self.assertIs(large_face, target["face"]) self.assertEqual((0.0, 0.0, 0.0), (target["point"].x, target["point"].y, target["point"].z)) def test_controller_moves_qet_device_root_when_selected_face_belongs_to_child_shape(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") cabinet = doc.addObject("Part::Feature", "CabinetPanel") device = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a") terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-a") child = doc.addObject("Part::Feature", "DeviceSolid") device.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation()) child.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation()) root.addObject(device) device.addObject(child) target_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, 1), ) moving_face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, -1), ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(0, 0, 0)], SubObjects=[target_face], SubElementNames=["Face1"], Object=cabinet, ), types.SimpleNamespace( PickedPoints=[app.Vector(0, 0, 9)], SubObjects=[moving_face], SubElementNames=["Face2"], Object=child, ), ] result = panel.ManualWiringController().align_selected_contact_faces() self.assertIs(device, result["moving_object"]) self.assertEqual((0.0, 0.0, 1.0), (device.Placement.Base.x, device.Placement.Base.y, device.Placement.Base.z)) self.assertEqual((0.0, 0.0, 0.0), (child.Placement.Base.x, child.Placement.Base.y, child.Placement.Base.z)) self.assertEqual([device], selection_state["selection"]) def test_controller_requires_two_faces_for_contact_alignment(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cabinet = doc.addObject("Part::Feature", "CabinetPanel") face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, 1), ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(100, 20, 0)], SubObjects=[face], SubElementNames=["Face1"], Object=cabinet, ) ] with self.assertRaisesRegex(panel.ManualWiringPanelError, "目标面"): panel.ManualWiringController().align_selected_contact_faces() def test_controller_rejects_more_than_two_faces_for_contact_alignment(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") cabinet = doc.addObject("Part::Feature", "CabinetPanel") rail = doc.addObject("Part::Feature", "DINRail") breaker = doc.addObject("Part::Feature", "Breaker") face = types.SimpleNamespace( ShapeType="Face", normalAt=lambda u, v: app.Vector(0, 0, 1), ) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(0, 0, 0)], SubObjects=[face], SubElementNames=["Face1"], Object=cabinet, ), types.SimpleNamespace( PickedPoints=[app.Vector(0, 0, 10)], SubObjects=[face], SubElementNames=["Face2"], Object=rail, ), types.SimpleNamespace( PickedPoints=[app.Vector(0, 0, 20)], SubObjects=[face], SubElementNames=["Face3"], Object=breaker, ), ] with self.assertRaisesRegex(panel.ManualWiringPanelError, "只能选择两个面"): panel.ManualWiringController().align_selected_contact_faces() def test_controller_deletes_last_waypoint_and_preview_point(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") controller = panel.ManualWiringController(terminal_exit_length=10.0) for point in [app.Vector(10, 20, 30), app.Vector(40, 50, 60)]: selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[point], SubObjects=[], SubElementNames=[], Object=types.SimpleNamespace(Name="CabinetFace", Label="柜体面"), ) ] controller.add_waypoint_from_selection() preview_group = doc.getObject("QETWiring_03_Previews") self.assertEqual(2, len(controller.waypoints)) self.assertEqual(2, len(controller.preview_objects)) self.assertEqual(2, len(preview_group.Group)) removed = controller.delete_last_waypoint() self.assertIsNotNone(removed) self.assertEqual(1, len(controller.waypoints)) self.assertEqual(1, len(controller.preview_objects)) self.assertEqual(1, len(preview_group.Group)) 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( [ (1.0, 2.0, 3.0), (1.0, 12.0, 3.0), (1.0, 12.0, 30.0), (1.0, 20.0, 30.0), (10.0, 20.0, 30.0), (10.0, 20.0, 17.0), (10.0, 8.0, 17.0), (9.0, 8.0, 17.0), (9.0, 8.0, 7.0), ], [(point.x, point.y, point.z) for point in getattr(wire, "Points", [])], ) def test_controller_diagnoses_last_generated_wire(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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", ) controller = panel.ManualWiringController() selection_state["selection"] = [start_terminal] controller.set_start_from_selection() selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(10, 10, 10)], SubObjects=[], SubElementNames=[], Object=types.SimpleNamespace(Name="线槽无对象名", Label="线槽"), ) ] controller.add_waypoint_from_selection() controller.waypoints[0]["source_object_name"] = "" selection_state["selection"] = [end_terminal] controller.set_end_from_selection_and_generate() diagnostics = controller.diagnose_last_wire() self.assertTrue( any(item["code"] == "wire_duct_source_missing" for item in diagnostics) ) def test_controller_writes_all_wire_diagnostics(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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", ) controller = panel.ManualWiringController() selection_state["selection"] = [start_terminal] controller.set_start_from_selection() controller.waypoints = [ { "point": app.Vector(10, 10, 10), "carrier_kind": "wire_duct", "carrier_axis": "x", } ] selection_state["selection"] = [end_terminal] controller.set_end_from_selection_and_generate() report = controller.diagnose_all_wires() self.assertEqual(1, report["issue_count"]) self.assertEqual(1, len(doc.getObject("QETWiring_05_Diagnostics").Group)) def test_controller_generates_wire_from_selected_task(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() wiring_objects = importlib.import_module("WiringObjects") 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", ) task = wiring_objects.create_wire_task( doc, "project-1", "wire-1", "W001", "terminal-start", "terminal-end", "instance-a", "instance-a", net_uuid="net-1", group_uuid="group-1", wire_mark="W001", wire_mark_is_manual=True, ) controller = panel.ManualWiringController(terminal_exit_length=10.0) controller.set_task_from_object(task) selection_state["selection_ex"] = [ types.SimpleNamespace( PickedPoints=[app.Vector(10, 20, 30)], SubObjects=[], SubElementNames=[], Object=types.SimpleNamespace(Name="CabinetFace", Label="柜体面"), ) ] controller.add_waypoint_from_selection() selection_state["selection"] = [] wire = controller.set_end_from_selection_and_generate() self.assertEqual("wire-1", getattr(wire, "QetWireUuid", "")) self.assertEqual("net-1", getattr(wire, "QetNetUuid", "")) self.assertEqual("group-1", getattr(wire, "QetGroupUuid", "")) self.assertEqual("W001", getattr(wire, "QetWireMark", "")) self.assertTrue(getattr(wire, "QetWireMarkIsManual", False)) self.assertEqual("terminal-start", getattr(wire, "QetStartTerminalUuid", "")) self.assertEqual("terminal-end", getattr(wire, "QetEndTerminalUuid", "")) self.assertEqual("Routed", getattr(task, "RouteStatus", "")) def test_controller_prefers_task_element_uuid_when_terminal_uuid_is_reused(self): _install_fake_freecad() terminal_objects, panel = _reload_modules() wiring_objects = importlib.import_module("WiringObjects") app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc root = terminal_objects.ensure_root_group(doc, "project-1") def add_device(element_uuid, instance_id, terminal_name): device = doc.addObject("App::DocumentObjectGroup", "QETDevice_" + element_uuid) root.addObject(device) terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", element_uuid) terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", instance_id) terminal = doc.addObject("Part::LocalCoordinateSystem", terminal_name) terminal.Placement = app.Placement(app.Vector(1, 2, 3), app.Rotation()) device.addObject(terminal) terminal_objects.set_terminal_semantics( terminal, "project-1", element_uuid, "terminal-reused", instance_id, label=terminal_name, ) return terminal wrong_start = add_device("device-a", "instance-a", "WrongStart") correct_start = add_device("device-b", "instance-b", "CorrectStart") correct_end = add_device("device-c", "instance-c", "CorrectEnd") _ = wrong_start task = wiring_objects.create_wire_task( doc, "project-1", "wire-1", "W001", "terminal-reused", "terminal-reused", "", "", ) terminal_objects.ensure_string_property( task, "QetStartElementUuid", "QET Wiring", "", "device-b", ) terminal_objects.ensure_string_property( task, "QetEndElementUuid", "QET Wiring", "", "device-c", ) controller = panel.ManualWiringController() controller.set_task_from_object(task) self.assertIs(correct_start, controller.start_terminal) self.assertIs(correct_end, panel._find_terminal_by_uuid(doc, "terminal-reused", element_uuid="device-c")) def test_controller_loads_selected_routed_wire_for_edit(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() manual_wiring = importlib.import_module("ManualWiring") app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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(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, 0), "support_axis": "z"}], terminal_exit_length=5.0, wire_uuid="wire-1", wire_label="W001", ) selection_state["selection"] = [wire] controller = panel.ManualWiringController() loaded = controller.load_selected_wire_for_edit() self.assertIs(wire, loaded) self.assertIs(wire, controller.editing_wire) self.assertIs(start_terminal, controller.start_terminal) self.assertEqual(5.0, controller.terminal_exit_length) self.assertEqual(1, len(controller.waypoints)) self.assertEqual( (40.0, 20.0, 0.0), ( controller.waypoints[0]["point"].x, controller.waypoints[0]["point"].y, controller.waypoints[0]["point"].z, ), ) self.assertEqual(1, len(controller.preview_objects)) def test_controller_updates_loaded_wire_in_transaction(self): selection_state = _install_fake_freecad() terminal_objects, panel = _reload_modules() manual_wiring = importlib.import_module("ManualWiring") app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc 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(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, 0)}], wire_uuid="wire-1", wire_label="W001", ) selection_state["selection"] = [wire] controller = panel.ManualWiringController() controller.load_selected_wire_for_edit() controller.waypoints = [{"point": app.Vector(70, 30, 10), "support_axis": "x"}] updated = controller.update_loaded_wire() routed_group = doc.getObject("QETWiring_04_Routed") self.assertIs(wire, updated) self.assertEqual([wire], routed_group.Group) self.assertTrue( any(point.x == 70.0 and point.y == 30.0 and point.z == 10.0 for point in wire.Points) ) waypoints = json.loads(getattr(wire, "QetManualWaypointsJson", "[]")) self.assertEqual("x", waypoints[0]["support_axis"]) self.assertEqual( [("open", "修改手动导线"), ("commit", "")], doc.transactions[-2:], ) def test_controller_undo_last_change_uses_document_undo(self): _install_fake_freecad() terminal_objects, panel = _reload_modules() app = sys.modules["FreeCAD"] doc = FakeDocument() app.ActiveDocument = doc terminal_objects.ensure_root_group(doc, "project-1") panel.ManualWiringController().undo_last_change() self.assertEqual(("undo", ""), doc.transactions[-1]) if __name__ == "__main__": unittest.main()