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.
428 lines
18 KiB
Python
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()
|