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_batch_asse...

428 lines
18 KiB
Python

import importlib
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 multVec(self, vector):
return vector
class Placement:
def __init__(self, base=None, rotation=None):
self.Base = base or Vector()
self.Rotation = rotation or Rotation()
fake_freecad = types.ModuleType("FreeCAD")
fake_freecad.Vector = Vector
fake_freecad.Rotation = Rotation
fake_freecad.Placement = Placement
fake_freecad.ActiveDocument = None
fake_freecad.Console = types.SimpleNamespace(
PrintMessage=lambda *args, **kwargs: None,
PrintWarning=lambda *args, **kwargs: None,
PrintError=lambda *args, **kwargs: None,
)
sys.modules["FreeCAD"] = fake_freecad
fake_importgui = types.ModuleType("ImportGui")
def insert(name, docName=None, merge=False, useLinkGroup=True):
doc = fake_freecad.ActiveDocument
obj = doc.addObject("Part::Feature", "ImportedBatchModel")
obj.ImportedPath = name
return obj
fake_importgui.insert = insert
sys.modules["ImportGui"] = fake_importgui
fake_freecadgui = types.ModuleType("FreeCADGui")
fake_freecadgui.addCommand = lambda *args, **kwargs: None
fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: [])
sys.modules["FreeCADGui"] = fake_freecadgui
class FakeViewObject:
def __init__(self):
self.Visibility = True
class FakeObject:
def __init__(self, name, type_id):
self.Name = name
self.Label = name
self.TypeId = type_id
self.PropertiesList = []
self.Group = []
self.InList = []
self.ViewObject = FakeViewObject()
self.Placement = sys.modules["FreeCAD"].Placement()
self.Shape = None
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 = "QETScene"
self.recompute_count = 0
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 recompute(self):
self.recompute_count += 1
def _reload_modules(*extra_names):
for name in ["RoutingNetwork", "WiringObjects", "TemplateSemantics", "TerminalObjects", "BatchAssembly"] + list(extra_names):
sys.modules.pop(name, None)
terminal_objects = importlib.import_module("TerminalObjects")
batch_assembly = importlib.import_module("BatchAssembly")
return terminal_objects, batch_assembly
class BatchAssemblyTest(unittest.TestCase):
def _qet_device(self, doc, terminal_objects, label, instance_id=None, element_uuid=None):
token = terminal_objects.safe_token(label)
device = doc.addObject("App::DocumentObjectGroup", "QETDevice_" + token)
device.Label = label
terminal_objects.ensure_string_property(device, "QetGroupKind", "QET Exchange", "", "Device")
terminal_objects.ensure_string_property(
device,
"QetElementUuid",
"QET Exchange",
"",
element_uuid or label,
)
terminal_objects.ensure_string_property(
device,
"QetInstanceId",
"QET Exchange",
"",
instance_id or label,
)
return device
def _terminal(self, doc, terminal_objects, device, terminal_uuid, label):
terminal_group = terminal_objects.ensure_terminal_group(
doc,
device,
project_uuid="project-1",
instance_id=device.QetInstanceId,
)
terminal = terminal_objects.create_lcs_object(
doc,
"QETTerminal_" + terminal_objects.safe_token(terminal_uuid),
label=label,
)
terminal_group.addObject(terminal)
terminal_objects.set_terminal_semantics(
terminal,
"project-1",
device.QetElementUuid,
terminal_uuid,
device.QetInstanceId,
label=label,
)
return terminal
def test_layout_existing_terminal_block_places_qet_terminal_slices_without_local_rebind(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
rail = doc.addObject("App::DocumentObjectGroup", "DINRail")
rail.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation())
terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail")
terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x")
ud2 = self._qet_device(doc, terminal_objects, "UD:2", instance_id="ud-2", element_uuid="element-ud-2")
ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1")
self._terminal(doc, terminal_objects, ud2, "element-ud-2:terminal-template-1", "UD:2")
self._terminal(doc, terminal_objects, ud1, "element-ud-1:terminal-template-1", "UD:1")
report = batch_assembly.layout_existing_terminal_block(
doc,
rail,
block_name="UD",
pitch_mm=5.2,
start_offset_mm=10.0,
)
self.assertEqual("qet_existing", report["source"])
self.assertEqual(2, report["updated_devices"])
self.assertEqual(0, report["created_devices"])
self.assertEqual(["UD:1", "UD:2"], [device.Label for device in report["devices"]])
self.assertEqual([110.0, 115.2], [device.Placement.Base.x for device in report["devices"]])
self.assertEqual(
["element-ud-1:terminal-template-1", "element-ud-2:terminal-template-1"],
[terminal.QetTerminalUuid for terminal in report["terminals"]],
)
self.assertFalse(any(terminal.QetTerminalUuid.startswith("local:") for terminal in report["terminals"]))
self.assertEqual("layout_existing", ud1.QetBatchAssemblyMode)
self.assertEqual("rail", ud1.QetMountHostKind)
def test_layout_existing_terminal_block_moves_group_children_for_document_groups(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
rail = doc.addObject("App::DocumentObjectGroup", "DINRail")
rail.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation())
terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail")
terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x")
ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1")
body = doc.addObject("Part::Feature", "TerminalSlice_GreenBody")
body.Placement = app.Placement(app.Vector(10, 20, 30), app.Rotation())
ud1.addObject(body)
self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1")
report = batch_assembly.layout_existing_terminal_block(
doc,
rail,
block_name="UD",
pitch_mm=5.2,
start_offset_mm=10.0,
)
self.assertEqual(1, report["updated_devices"])
self.assertNotEqual(10.0, body.Placement.Base.x)
self.assertAlmostEqual(110.0, ud1.Placement.Base.x)
def test_available_terminal_strip_names_comes_from_existing_qet_devices(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules()
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1")
id2 = self._qet_device(doc, terminal_objects, "ID:2", instance_id="id-2", element_uuid="element-id-2")
self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1")
self._terminal(doc, terminal_objects, id2, "terminal-id-2", "ID:2")
self.assertEqual(["ID", "UD"], batch_assembly.available_terminal_strip_names(doc))
def test_layout_existing_devices_filters_qet_breakers_and_ignores_terminal_slices(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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")
qf2 = self._qet_device(doc, terminal_objects, "QF2", instance_id="qf-2", element_uuid="element-qf-2")
qf1 = self._qet_device(doc, terminal_objects, "QF1", instance_id="qf-1", element_uuid="element-qf-1")
qf2_terminal = self._terminal(doc, terminal_objects, qf2, "terminal-qf-2-1", "QF2:1")
qf1_terminal = self._terminal(doc, terminal_objects, qf1, "terminal-qf-1-1", "QF1:1")
ta1 = self._qet_device(doc, terminal_objects, "TA1", instance_id="ta-1", element_uuid="element-ta-1")
ud1 = self._qet_device(doc, terminal_objects, "UD:1", instance_id="ud-1", element_uuid="element-ud-1")
self._terminal(doc, terminal_objects, ud1, "terminal-ud-1", "UD:1")
qf0 = self._qet_device(doc, terminal_objects, "QF0", instance_id="qf-0", element_uuid="element-qf-0")
terminal_objects.ensure_string_property(
qf0,
"QetBatchAssemblyKind",
"QET Batch Assembly",
"",
"breaker_batch",
)
report = batch_assembly.layout_existing_devices(
doc,
rail,
prefix="QF",
pitch_mm=18.0,
start_offset_mm=5.0,
kind="breaker_batch",
)
self.assertEqual("qet_existing", report["source"])
self.assertEqual(["QF1", "QF2"], [device.Label for device in report["devices"]])
self.assertNotIn(ta1, report["devices"])
self.assertNotIn(ud1, report["devices"])
self.assertNotIn(qf0, report["devices"])
self.assertNotIn(qf1_terminal, report["devices"])
self.assertNotIn(qf2_terminal, report["devices"])
self.assertEqual([5.0, 23.0], [device.Placement.Base.x for device in report["devices"]])
self.assertEqual("layout_existing", qf1.QetBatchAssemblyMode)
def test_create_terminal_block_places_slices_and_local_terminals_along_selected_rail(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
rail = doc.addObject("App::DocumentObjectGroup", "DINRail")
rail.Placement = app.Placement(app.Vector(100, 10, 5), app.Rotation())
terminal_objects.ensure_string_property(rail, "QetCarrierKind", "QET Wiring", "", "rail")
terminal_objects.ensure_string_property(rail, "QetCarrierAxis", "QET Wiring", "", "x")
report = batch_assembly.create_terminal_block(
doc,
rail,
block_name="XT1",
count=3,
pitch_mm=5.2,
start_offset_mm=10.0,
)
self.assertEqual(3, report["created_devices"])
self.assertEqual(3, report["created_terminals"])
self.assertEqual("XT1", report["group"].Label)
placements = [item.Placement.Base.x for item in report["devices"]]
self.assertEqual([110.0, 115.2, 120.4], placements)
terminal_labels = [terminal.Label for terminal in report["terminals"]]
self.assertEqual(["XT1:1", "XT1:2", "XT1:3"], terminal_labels)
self.assertTrue(all(terminal.QetTerminalUuid.startswith("local:") for terminal in report["terminals"]))
self.assertTrue(all(not terminal.ViewObject.Visibility for terminal in report["terminals"]))
def test_create_breakers_generates_numbered_devices_and_terminal_labels(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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")
report = batch_assembly.create_breakers(
doc,
rail,
base_name="QF",
count=2,
pitch_mm=18.0,
start_offset_mm=0.0,
terminal_numbers=("1", "2", "3", "4", "5", "6"),
)
self.assertEqual(["QF1", "QF2"], [device.Label for device in report["devices"]])
self.assertEqual(12, report["created_terminals"])
self.assertTrue(all(device.Name.startswith("QETDevice_") for device in report["devices"]))
self.assertEqual(["QF1", "QF2"], [device.QetInstanceId for device in report["devices"]])
self.assertEqual(["QF1", "QF2"], [device.QetElementUuid for device in report["devices"]])
labels = [terminal.Label for terminal in report["terminals"]]
self.assertEqual(["QF1:1", "QF1:2", "QF1:3", "QF1:4", "QF1:5", "QF1:6"], labels[:6])
self.assertEqual(["QF2:1", "QF2:2", "QF2:3", "QF2:4", "QF2:5", "QF2:6"], labels[6:])
self.assertEqual([0.0, 18.0], [device.Placement.Base.x for device in report["devices"]])
def test_created_breaker_local_slots_can_be_promoted_to_qet_terminal_uuid(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _reload_modules("AutoRouting")
auto_routing = importlib.import_module("AutoRouting")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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")
batch_assembly.create_breakers(
doc,
rail,
base_name="QF",
count=1,
terminal_numbers=("1", "2"),
)
payload = {
"project_uuid": "project-1",
"wires": [
{
"wire_uuid": "wire-1",
"start_instance_id": "QF1",
"start_terminal_uuid": "terminal-qf1-1",
"start_terminal_display": "1",
"end_instance_id": "QF1",
"end_terminal_uuid": "terminal-qf1-2",
"end_terminal_display": "2",
}
],
}
report = auto_routing.bind_wire_task_terminals_from_payload(doc, payload)
self.assertEqual(2, report["bound"])
indexed = auto_routing.index_terminals(doc)
self.assertIn("terminal-qf1-1", indexed)
self.assertIn("terminal-qf1-2", indexed)
self.assertFalse(any(key.startswith("local:QF1") for key in indexed))
def test_create_breakers_can_import_model_template_instead_of_placeholder_box(self):
_install_fake_freecad()
terminal_objects, batch_assembly = _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")
with tempfile.TemporaryDirectory() as temp_dir:
model_path = Path(temp_dir) / "breaker.step"
model_path.write_text("fake step", encoding="utf-8")
report = batch_assembly.create_breakers(
doc,
rail,
base_name="QF",
count=1,
model_path=str(model_path),
)
imported_children = [child for child in report["devices"][0].Group if getattr(child, "ImportedPath", "")]
self.assertEqual(1, len(imported_children))
self.assertEqual(str(model_path), imported_children[0].QetBatchSourceModelPath)
self.assertEqual("QF1 模型", imported_children[0].Label)
if __name__ == "__main__":
unittest.main()