You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
LightWork3D/tests/python/freecad_exchange_manual_wir...

1900 lines
72 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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": [],
"commands": [],
"control_events": [],
}
fake_freecadgui = types.ModuleType("FreeCADGui")
fake_freecadgui.addCommand = lambda *args, **kwargs: None
fake_freecadgui.runCommand = lambda command: selection_state["commands"].append(command)
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: selection_state["control_events"].append("closeDialog"),
)
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,23 4;56",
)
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_refreshes_face_contact_mount_when_changing_carrier_length(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")
carrier = doc.addObject("App::DocumentObjectGroup", "WireDuctCarrier")
cabinet.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation())
carrier.Placement = app.Placement(app.Vector(120, 0, 5), app.Rotation())
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
terminal_objects.ensure_string_property(
carrier,
"QetMountMode",
"QET Assembly",
"QET cabinet assembly mount metadata",
"face_contact",
)
terminal_objects.ensure_string_property(
carrier,
"QetMountHostName",
"QET Assembly",
"QET cabinet assembly mount metadata",
"CabinetPanel",
)
terminal_objects.ensure_string_property(
carrier,
"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())
selection_state["selection"] = [carrier]
updated = panel.ManualWiringController().apply_length_to_selected_carriers(500.0)
self.assertEqual([carrier], updated)
self.assertEqual(500.0, carrier.QetCarrierLength)
self.assertEqual((150.0, 0.0, 5.0), (carrier.Placement.Base.x, carrier.Placement.Base.y, carrier.Placement.Base.z))
self.assertEqual({"x": 130.0, "y": 0.0, "z": 0.0}, json.loads(carrier.QetMountHostBaseJson))
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))
self.assertEqual({"x": 0.0, "y": 0.0, "z": 1.0}, json.loads(rail.QetMountHostNormalJson))
def test_controller_aligns_contact_faces_with_configured_normal_offset(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(contact_offset_mm=2.0).align_selected_contact_faces()
self.assertIs(rail, result["moving_object"])
self.assertEqual((0.0, 0.0, 3.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z))
self.assertEqual((-0.0, -0.0, -7.0), (result["translation"].x, result["translation"].y, result["translation"].z))
self.assertEqual(2.0, result["contact_offset_mm"])
self.assertEqual(2.0, rail.QetMountOffsetMm)
def test_controller_applies_contact_offset_to_selected_mounted_object(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,
),
]
controller = panel.ManualWiringController()
controller.align_selected_contact_faces()
selection_state["selection"] = [rail]
updated = controller.apply_contact_offset_to_selected_mounts(5.0)
self.assertEqual([rail], updated)
self.assertEqual((0.0, 0.0, 6.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z))
self.assertEqual(5.0, rail.QetMountOffsetMm)
self.assertEqual({"x": 0.0, "y": 0.0, "z": 6.0}, json.loads(rail.QetMountLocalBaseJson))
def test_controller_reverses_contact_normal_for_selected_mounted_object(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,
),
]
controller = panel.ManualWiringController()
controller.align_selected_contact_faces()
selection_state["selection"] = [rail]
reversed_objects = controller.reverse_contact_normal_for_selected_mounts()
updated = controller.apply_contact_offset_to_selected_mounts(5.0)
self.assertEqual([rail], reversed_objects)
self.assertEqual([rail], updated)
self.assertEqual({"x": -0.0, "y": -0.0, "z": -1.0}, json.loads(rail.QetMountHostNormalJson))
self.assertEqual((0.0, 0.0, -4.0), (rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z))
def test_controller_requires_saved_contact_normal_before_applying_offset(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("Part::Feature", "DINRail")
rail.Placement = app.Placement(app.Vector(0, 0, 1), app.Rotation())
terminal_objects.ensure_string_property(
rail,
"QetMountMode",
"QET Assembly",
"QET cabinet assembly mount metadata",
"face_contact",
)
selection_state["selection"] = [rail]
with self.assertRaisesRegex(panel.ManualWiringPanelError, "贴合法向"):
panel.ManualWiringController().apply_contact_offset_to_selected_mounts(3.0)
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_rejects_multiple_moving_faces_after_target_face_is_set(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")
target_face = types.SimpleNamespace(
ShapeType="Face",
normalAt=lambda u, v: app.Vector(0, 0, 1),
)
moving_face_a = types.SimpleNamespace(
ShapeType="Face",
normalAt=lambda u, v: app.Vector(0, 0, -1),
)
moving_face_b = 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(0, 0, 0)],
SubObjects=[target_face],
SubElementNames=["Face1"],
Object=cabinet,
)
]
controller.set_contact_target_from_selection()
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(0, 0, 9), app.Vector(0, 1, 9)],
SubObjects=[moving_face_a, moving_face_b],
SubElementNames=["Face2", "Face3"],
Object=rail,
)
]
with self.assertRaisesRegex(panel.ManualWiringPanelError, "只选择一个"):
controller.align_selected_contact_faces()
def test_controller_moves_plain_app_part_parent_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
terminal_objects.ensure_root_group(doc, "project-1")
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
assembly = doc.addObject("App::Part", "ImportedFcstdDevice")
child = doc.addObject("Part::Feature", "ImportedFcstdBody")
assembly.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation())
child.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
assembly.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(assembly, result["moving_object"])
self.assertEqual((0.0, 0.0, 1.0), (assembly.Placement.Base.x, assembly.Placement.Base.y, assembly.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([assembly], 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])
def test_controller_closes_panel_and_launches_native_transform(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")
device = doc.addObject("Part::Feature", "Device")
selection_state["selection"] = [device]
result = panel.ManualWiringController().launch_native_transform_for_selection()
self.assertTrue(result)
self.assertEqual(["closeDialog"], selection_state["control_events"])
self.assertEqual(["Std_TransformManip"], selection_state["commands"])
def test_controller_requires_selection_before_native_transform(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")
selection_state["selection"] = []
with self.assertRaisesRegex(panel.ManualWiringPanelError, "请选择要变换的对象"):
panel.ManualWiringController().launch_native_transform_for_selection()
self.assertEqual([], selection_state["control_events"])
self.assertEqual([], selection_state["commands"])
if __name__ == "__main__":
unittest.main()