feat(freecad): improve qet assembly placement workflow
parent
5dbd39747b
commit
b0c662bd11
@ -0,0 +1,515 @@
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
import TerminalObjects
|
||||
|
||||
try:
|
||||
import ImportGui
|
||||
except Exception:
|
||||
ImportGui = None
|
||||
|
||||
|
||||
class BatchAssemblyError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _project_uuid(doc):
|
||||
try:
|
||||
root = TerminalObjects.ensure_root_group(doc)
|
||||
return (getattr(root, "QetProjectUuid", "") or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _safe_label(text, fallback):
|
||||
value = str(text or "").strip()
|
||||
return value or fallback
|
||||
|
||||
|
||||
def _axis_vector(rail):
|
||||
axis = (getattr(rail, "QetCarrierAxis", "") or "x").strip().lower()
|
||||
if axis == "y":
|
||||
vector = App.Vector(0, 1, 0)
|
||||
elif axis == "z":
|
||||
vector = App.Vector(0, 0, 1)
|
||||
else:
|
||||
vector = App.Vector(1, 0, 0)
|
||||
|
||||
try:
|
||||
placement = getattr(rail, "Placement", None)
|
||||
rotation = getattr(placement, "Rotation", None)
|
||||
if rotation is not None and hasattr(rotation, "multVec"):
|
||||
vector = rotation.multVec(vector)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
length = math.sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)
|
||||
if length <= 1e-9:
|
||||
return App.Vector(1, 0, 0)
|
||||
return App.Vector(vector.x / length, vector.y / length, vector.z / length)
|
||||
|
||||
|
||||
def _base_point(rail):
|
||||
placement = getattr(rail, "Placement", None)
|
||||
base = getattr(placement, "Base", None)
|
||||
if base is None:
|
||||
return App.Vector(0, 0, 0)
|
||||
return App.Vector(base.x, base.y, base.z)
|
||||
|
||||
|
||||
def _point_at(base, axis, offset):
|
||||
return App.Vector(
|
||||
base.x + axis.x * float(offset or 0.0),
|
||||
base.y + axis.y * float(offset or 0.0),
|
||||
base.z + axis.z * float(offset or 0.0),
|
||||
)
|
||||
|
||||
|
||||
def _placement_at(rail, point):
|
||||
rotation = None
|
||||
try:
|
||||
rotation = getattr(getattr(rail, "Placement", None), "Rotation", None)
|
||||
except Exception:
|
||||
rotation = None
|
||||
return App.Placement(point, rotation or App.Rotation())
|
||||
|
||||
|
||||
def _ensure_rail(rail):
|
||||
if rail is None:
|
||||
raise BatchAssemblyError("请先选择一根导轨。")
|
||||
kind = (getattr(rail, "QetCarrierKind", "") or "").strip()
|
||||
if kind and kind != "rail":
|
||||
raise BatchAssemblyError("所选对象不是导轨。")
|
||||
return rail
|
||||
|
||||
|
||||
def _ensure_batch_root(doc, project_uuid=""):
|
||||
group = doc.getObject("QETBatchAssembly")
|
||||
if group is None:
|
||||
group = doc.addObject("App::DocumentObjectGroup", "QETBatchAssembly")
|
||||
group.Label = "QET Batch Assembly"
|
||||
if project_uuid:
|
||||
TerminalObjects.ensure_string_property(
|
||||
group,
|
||||
"QetProjectUuid",
|
||||
"QET Batch Assembly",
|
||||
"Project UUID",
|
||||
project_uuid,
|
||||
)
|
||||
return group
|
||||
|
||||
|
||||
def _unique_object_name(doc, base_name):
|
||||
name = TerminalObjects.safe_token(base_name) or "QETObject"
|
||||
if doc.getObject(name) is None:
|
||||
return name
|
||||
|
||||
suffix = 1
|
||||
while doc.getObject("{0}_{1}".format(name, suffix)) is not None:
|
||||
suffix += 1
|
||||
return "{0}_{1}".format(name, suffix)
|
||||
|
||||
|
||||
def _existing_object_names(doc):
|
||||
return {getattr(obj, "Name", "") for obj in list(getattr(doc, "Objects", []) or [])}
|
||||
|
||||
|
||||
def _new_objects_since(doc, before_names):
|
||||
return [
|
||||
obj
|
||||
for obj in list(getattr(doc, "Objects", []) or [])
|
||||
if getattr(obj, "Name", "") not in before_names
|
||||
]
|
||||
|
||||
|
||||
def _top_level_objects(objects):
|
||||
object_set = set(objects or [])
|
||||
result = []
|
||||
for obj in objects or []:
|
||||
parents = set(getattr(obj, "InList", []) or [])
|
||||
if parents.intersection(object_set):
|
||||
continue
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
def _supported_model_path(path):
|
||||
suffix = Path(path or "").suffix.lower()
|
||||
return suffix in {".fcstd", ".step", ".stp"}
|
||||
|
||||
|
||||
def _set_source_model_path(obj, model_path):
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetBatchSourceModelPath",
|
||||
"QET Batch Assembly",
|
||||
"Batch assembly imported model path",
|
||||
model_path,
|
||||
)
|
||||
|
||||
|
||||
def _import_fcstd_objects(doc, path):
|
||||
if not hasattr(App, "openDocument") or not hasattr(doc, "copyObject"):
|
||||
return []
|
||||
source_doc = None
|
||||
try:
|
||||
source_doc = App.openDocument(path, hidden=True, temporary=True)
|
||||
copied = []
|
||||
for source_obj in _top_level_objects(list(getattr(source_doc, "Objects", []) or [])):
|
||||
copied.append(doc.copyObject(source_obj, True))
|
||||
return copied
|
||||
finally:
|
||||
if source_doc is not None and hasattr(App, "closeDocument"):
|
||||
try:
|
||||
App.closeDocument(source_doc.Name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _import_step_objects(doc, path):
|
||||
if ImportGui is None:
|
||||
return []
|
||||
before = _existing_object_names(doc)
|
||||
try:
|
||||
ImportGui.insert(name=path, docName=doc.Name, merge=False, useLinkGroup=True)
|
||||
except TypeError:
|
||||
ImportGui.insert(path, doc.Name)
|
||||
return _top_level_objects(_new_objects_since(doc, before))
|
||||
|
||||
|
||||
def _import_model_objects(doc, model_path):
|
||||
path = str(model_path or "").strip()
|
||||
if not path:
|
||||
return []
|
||||
if not _supported_model_path(path):
|
||||
raise BatchAssemblyError("请选择 STEP/STP/FCStd 模型文件。")
|
||||
if not Path(path).is_file():
|
||||
raise BatchAssemblyError("模型文件不存在:{0}".format(path))
|
||||
suffix = Path(path).suffix.lower()
|
||||
if suffix == ".fcstd":
|
||||
return _import_fcstd_objects(doc, path)
|
||||
return _import_step_objects(doc, path)
|
||||
|
||||
|
||||
def _set_batch_properties(obj, kind, batch_name, host):
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetBatchAssemblyKind",
|
||||
"QET Batch Assembly",
|
||||
"Batch assembly kind",
|
||||
kind,
|
||||
)
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetBatchAssemblyName",
|
||||
"QET Batch Assembly",
|
||||
"Batch assembly name",
|
||||
batch_name,
|
||||
)
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetMountKind",
|
||||
"QET Mount",
|
||||
"Mount kind",
|
||||
"rail",
|
||||
)
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetMountHostName",
|
||||
"QET Mount",
|
||||
"Mount host object name",
|
||||
getattr(host, "Name", "") or "",
|
||||
)
|
||||
|
||||
|
||||
def _set_qet_device_properties(obj, project_uuid, element_uuid, instance_id):
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetGroupKind",
|
||||
"QET Exchange",
|
||||
"FreeCADExchange group kind",
|
||||
"Device",
|
||||
)
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetProjectUuid",
|
||||
"QET Exchange",
|
||||
"Project UUID from QET exchange",
|
||||
project_uuid,
|
||||
)
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetElementUuid",
|
||||
"QET Exchange",
|
||||
"Parent element UUID from QET exchange",
|
||||
element_uuid,
|
||||
)
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
"QetInstanceId",
|
||||
"QET Exchange",
|
||||
"Parent instance id from QET exchange",
|
||||
instance_id,
|
||||
)
|
||||
|
||||
|
||||
def _create_device_group(
|
||||
doc,
|
||||
parent,
|
||||
label,
|
||||
placement,
|
||||
kind,
|
||||
batch_name,
|
||||
host,
|
||||
project_uuid,
|
||||
element_uuid,
|
||||
instance_id,
|
||||
):
|
||||
group = doc.addObject(
|
||||
"App::DocumentObjectGroup",
|
||||
_unique_object_name(doc, "QETDevice_{0}".format(label)),
|
||||
)
|
||||
group.Label = label
|
||||
try:
|
||||
group.Placement = placement
|
||||
except Exception:
|
||||
pass
|
||||
parent.addObject(group)
|
||||
_set_batch_properties(group, kind, batch_name, host)
|
||||
_set_qet_device_properties(group, project_uuid, element_uuid, instance_id)
|
||||
return group
|
||||
|
||||
|
||||
def _create_visual_placeholder(doc, device_group, label, placement, kind):
|
||||
try:
|
||||
body = doc.addObject("Part::Feature", "QETBatchBody_{0}".format(TerminalObjects.safe_token(label)))
|
||||
body.Label = "{0} 模型".format(label)
|
||||
body.Placement = placement
|
||||
try:
|
||||
import Part
|
||||
|
||||
if kind == "breaker":
|
||||
body.Shape = Part.makeBox(18.0, 72.0, 80.0)
|
||||
else:
|
||||
body.Shape = Part.makeBox(5.2, 40.0, 45.0)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if kind == "breaker":
|
||||
body.ViewObject.ShapeColor = (0.78, 0.78, 0.72)
|
||||
else:
|
||||
body.ViewObject.ShapeColor = (0.35, 0.75, 0.35)
|
||||
except Exception:
|
||||
pass
|
||||
device_group.addObject(body)
|
||||
_set_batch_properties(body, kind, getattr(device_group, "Label", "") or label, device_group)
|
||||
return body
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _create_visual_model(doc, device_group, label, placement, kind, model_path=""):
|
||||
imported = _import_model_objects(doc, model_path)
|
||||
if imported:
|
||||
for index, obj in enumerate(imported):
|
||||
try:
|
||||
obj.Placement = placement
|
||||
except Exception:
|
||||
pass
|
||||
if index == 0:
|
||||
obj.Label = "{0} 模型".format(label)
|
||||
device_group.addObject(obj)
|
||||
_set_batch_properties(obj, kind, getattr(device_group, "Label", "") or label, device_group)
|
||||
_set_source_model_path(obj, model_path)
|
||||
return imported[0]
|
||||
return _create_visual_placeholder(doc, device_group, label, placement, kind)
|
||||
|
||||
|
||||
def _create_terminal(
|
||||
doc,
|
||||
device_group,
|
||||
project_uuid,
|
||||
element_uuid,
|
||||
instance_id,
|
||||
terminal_no,
|
||||
offset_index=0,
|
||||
label_prefix="",
|
||||
):
|
||||
owner_label = _safe_label(label_prefix, getattr(device_group, "Label", "") or instance_id)
|
||||
label = "{0}:{1}".format(owner_label, terminal_no)
|
||||
base = getattr(getattr(device_group, "Placement", None), "Base", App.Vector())
|
||||
terminal_point = App.Vector(base.x, base.y + float(offset_index) * 2.0, base.z)
|
||||
terminal = TerminalObjects.create_lcs_object(
|
||||
doc,
|
||||
"QETTerminal_{0}_{1}".format(TerminalObjects.safe_token(instance_id), TerminalObjects.safe_token(terminal_no)),
|
||||
placement=App.Placement(terminal_point, App.Rotation()),
|
||||
label=label,
|
||||
)
|
||||
terminal_group = TerminalObjects.ensure_terminal_group(
|
||||
doc,
|
||||
device_group,
|
||||
project_uuid=project_uuid,
|
||||
instance_id=instance_id,
|
||||
)
|
||||
terminal_group.addObject(terminal)
|
||||
TerminalObjects.set_terminal_semantics(
|
||||
terminal,
|
||||
project_uuid,
|
||||
element_uuid,
|
||||
"local:{0}:{1}".format(instance_id, terminal_no),
|
||||
instance_id,
|
||||
label=label,
|
||||
slot_name=str(terminal_no),
|
||||
)
|
||||
TerminalObjects.hide_engineering_terminal(terminal)
|
||||
return terminal
|
||||
|
||||
|
||||
def _batch_report(kind, group, devices, terminals):
|
||||
return {
|
||||
"kind": kind,
|
||||
"group": group,
|
||||
"devices": devices,
|
||||
"terminals": terminals,
|
||||
"created_devices": len(devices),
|
||||
"created_terminals": len(terminals),
|
||||
}
|
||||
|
||||
|
||||
def create_terminal_block(
|
||||
doc,
|
||||
rail,
|
||||
block_name="XT1",
|
||||
count=10,
|
||||
pitch_mm=5.2,
|
||||
start_offset_mm=0.0,
|
||||
model_path="",
|
||||
):
|
||||
if doc is None:
|
||||
raise BatchAssemblyError("请先打开 FreeCAD 工程。")
|
||||
rail = _ensure_rail(rail)
|
||||
count = int(count or 0)
|
||||
if count <= 0:
|
||||
raise BatchAssemblyError("端子数量必须大于 0。")
|
||||
|
||||
project_uuid = _project_uuid(doc)
|
||||
batch_name = _safe_label(block_name, "XT1")
|
||||
root = _ensure_batch_root(doc, project_uuid)
|
||||
group = doc.addObject("App::DocumentObjectGroup", TerminalObjects.safe_token(batch_name))
|
||||
group.Label = batch_name
|
||||
root.addObject(group)
|
||||
_set_batch_properties(group, "terminal_block", batch_name, rail)
|
||||
|
||||
base = _base_point(rail)
|
||||
axis = _axis_vector(rail)
|
||||
devices = []
|
||||
terminals = []
|
||||
for index in range(count):
|
||||
terminal_no = str(index + 1)
|
||||
point = _point_at(base, axis, float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0))
|
||||
device_label = "{0}_{1:03d}".format(batch_name, index + 1)
|
||||
device = _create_device_group(
|
||||
doc,
|
||||
group,
|
||||
device_label,
|
||||
_placement_at(rail, point),
|
||||
"terminal_slice",
|
||||
batch_name,
|
||||
rail,
|
||||
project_uuid,
|
||||
device_label,
|
||||
device_label,
|
||||
)
|
||||
_create_visual_model(doc, device, device_label, _placement_at(rail, point), "terminal_slice", model_path)
|
||||
instance_id = device_label
|
||||
element_uuid = batch_name
|
||||
terminal = _create_terminal(
|
||||
doc,
|
||||
device,
|
||||
project_uuid,
|
||||
element_uuid,
|
||||
instance_id,
|
||||
terminal_no,
|
||||
label_prefix=batch_name,
|
||||
)
|
||||
devices.append(device)
|
||||
terminals.append(terminal)
|
||||
|
||||
try:
|
||||
doc.recompute()
|
||||
except Exception:
|
||||
pass
|
||||
return _batch_report("terminal_block", group, devices, terminals)
|
||||
|
||||
|
||||
def create_breakers(
|
||||
doc,
|
||||
rail,
|
||||
base_name="QF",
|
||||
count=3,
|
||||
pitch_mm=18.0,
|
||||
start_offset_mm=0.0,
|
||||
terminal_numbers=("1", "2", "3", "4", "5", "6"),
|
||||
model_path="",
|
||||
):
|
||||
if doc is None:
|
||||
raise BatchAssemblyError("请先打开 FreeCAD 工程。")
|
||||
rail = _ensure_rail(rail)
|
||||
count = int(count or 0)
|
||||
if count <= 0:
|
||||
raise BatchAssemblyError("断路器数量必须大于 0。")
|
||||
terminal_numbers = [str(item).strip() for item in terminal_numbers or () if str(item).strip()]
|
||||
if not terminal_numbers:
|
||||
raise BatchAssemblyError("至少需要一个端子号。")
|
||||
|
||||
project_uuid = _project_uuid(doc)
|
||||
batch_name = _safe_label(base_name, "QF")
|
||||
root = _ensure_batch_root(doc, project_uuid)
|
||||
group = doc.addObject("App::DocumentObjectGroup", "QETBatch_{0}".format(TerminalObjects.safe_token(batch_name)))
|
||||
group.Label = "{0} 批量断路器".format(batch_name)
|
||||
root.addObject(group)
|
||||
_set_batch_properties(group, "breaker_batch", batch_name, rail)
|
||||
|
||||
base = _base_point(rail)
|
||||
axis = _axis_vector(rail)
|
||||
devices = []
|
||||
terminals = []
|
||||
for index in range(count):
|
||||
device_label = "{0}{1}".format(batch_name, index + 1)
|
||||
point = _point_at(base, axis, float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0))
|
||||
device = _create_device_group(
|
||||
doc,
|
||||
group,
|
||||
device_label,
|
||||
_placement_at(rail, point),
|
||||
"breaker",
|
||||
batch_name,
|
||||
rail,
|
||||
project_uuid,
|
||||
device_label,
|
||||
device_label,
|
||||
)
|
||||
_create_visual_model(doc, device, device_label, _placement_at(rail, point), "breaker", model_path)
|
||||
instance_id = device_label
|
||||
element_uuid = device_label
|
||||
for terminal_index, terminal_no in enumerate(terminal_numbers):
|
||||
terminals.append(
|
||||
_create_terminal(
|
||||
doc,
|
||||
device,
|
||||
project_uuid,
|
||||
element_uuid,
|
||||
instance_id,
|
||||
terminal_no,
|
||||
offset_index=terminal_index,
|
||||
)
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
try:
|
||||
doc.recompute()
|
||||
except Exception:
|
||||
pass
|
||||
return _batch_report("breaker_batch", group, devices, terminals)
|
||||
@ -0,0 +1,260 @@
|
||||
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 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()
|
||||
Loading…
Reference in New Issue