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.

1019 lines
29 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 math
import re
from pathlib import Path
import FreeCAD as App
import TerminalObjects
try:
import ImportGui
except Exception:
ImportGui = None
class BatchAssemblyError(RuntimeError):
pass
TERMINAL_STRIP_NAME_PROPERTIES = (
"QetTerminalStripName",
"QetTerminalBlockName",
"QetTerminalGroupName",
"QetStripName",
"QetParentTerminalBlockName",
)
TERMINAL_STRIP_ORDER_PROPERTIES = (
"QetTerminalStripIndex",
"QetTerminalIndex",
"QetTerminalSequence",
"QetTerminalOrder",
"QetTerminalNo",
"QetTerminalDisplay",
)
DEVICE_PREFIX_PROPERTIES = (
"QetDeviceTag",
"QetDeviceName",
"QetDisplayTag",
"QetSymbolLabel",
"QetInstanceId",
"QetElementUuid",
)
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 _text_values(obj, include_children=False):
values = []
for attr_name in ("Label", "Name"):
value = (getattr(obj, attr_name, "") or "").strip()
if value:
values.append(value)
for prop_name in DEVICE_PREFIX_PROPERTIES + TERMINAL_STRIP_NAME_PROPERTIES:
value = (getattr(obj, prop_name, "") or "").strip()
if value:
values.append(value)
if include_children:
for child in list(getattr(obj, "Group", []) or []):
for attr_name in ("Label", "Name"):
value = (getattr(child, attr_name, "") or "").strip()
if value:
values.append(value)
return values
def _label_name_values(obj):
values = []
for attr_name in ("Label", "Name"):
value = (getattr(obj, attr_name, "") or "").strip()
if value:
values.append(value)
return values
def _natural_sort_key(value):
text = str(value or "")
key = []
for part in re.split(r"(\d+)", text):
if part.isdigit():
key.append((0, int(part)))
else:
key.append((1, part.lower()))
return key
def _parse_strip_name_and_order(obj):
for prop_name in TERMINAL_STRIP_NAME_PROPERTIES:
strip_name = (getattr(obj, prop_name, "") or "").strip()
if not strip_name:
continue
order = _explicit_order(obj)
if order is None:
order = _order_from_texts(_text_values(obj))
return strip_name, order
for text in _label_name_values(obj):
# Examples from QET trees: UD:1, UD-2, ID_006.
match = re.match(r"^\s*([A-Za-z][A-Za-z0-9]{0,8})\s*[:_\-]\s*(\d+)\b", text)
if match:
return match.group(1), int(match.group(2))
return "", None
def _explicit_order(obj):
for prop_name in TERMINAL_STRIP_ORDER_PROPERTIES:
value = (getattr(obj, prop_name, "") or "").strip()
if not value:
continue
match = re.search(r"\d+", value)
if match:
return int(match.group(0))
return None
def _order_from_texts(texts):
for text in texts:
match = re.search(r"(\d+)(?!.*\d)", str(text or ""))
if match:
return int(match.group(1))
return None
def _is_group_like(obj):
try:
return bool(obj and obj.isDerivedFrom("App::DocumentObjectGroup"))
except Exception:
return bool(getattr(obj, "Group", None) is not None)
def _qet_identity(obj):
instance_id = (getattr(obj, "QetInstanceId", "") or "").strip()
element_uuid = (getattr(obj, "QetElementUuid", "") or "").strip()
return instance_id, element_uuid
def _is_qet_device_object(obj):
if obj is None:
return False
if TerminalObjects.is_terminal_object(obj):
return False
group_kind = (getattr(obj, "QetGroupKind", "") or "").strip()
if group_kind in {TerminalObjects.TERMINAL_GROUP_KIND, TerminalObjects.WIRE_GROUP_KIND}:
return False
name = getattr(obj, "Name", "") or ""
if name.startswith(TerminalObjects.TERMINAL_GROUP_PREFIX) or name.startswith(TerminalObjects.WIRE_GROUP_PREFIX):
return False
instance_id, element_uuid = _qet_identity(obj)
if name.startswith(TerminalObjects.DEVICE_GROUP_PREFIX):
return True
if group_kind == "Device":
return True
return bool(instance_id or element_uuid)
def _contains_terminal_slice_geometry(obj):
text = " ".join(_text_values(obj, include_children=True)).lower()
return any(
token in text
for token in (
"terminalslice",
"terminal_slice",
"terminal slice",
"get_terminal_slice",
"端子片",
"端子排",
)
)
def _contains_qet_terminal_group(obj):
for child in list(getattr(obj, "Group", []) or []):
if (getattr(child, "QetGroupKind", "") or "").strip() == TerminalObjects.TERMINAL_GROUP_KIND:
return True
if getattr(child, "Name", "").startswith(TerminalObjects.TERMINAL_GROUP_PREFIX):
return True
return False
def _is_terminal_strip_device(obj):
if not _is_qet_device_object(obj):
return False
strip_name, _order = _parse_strip_name_and_order(obj)
if not strip_name:
return False
if _contains_terminal_slice_geometry(obj):
return True
if _contains_qet_terminal_group(obj):
return True
return False
def _matches_prefix(obj, prefix):
prefix = (prefix or "").strip().lower()
if not prefix:
return True
for text in _text_values(obj):
if text.lower().startswith(prefix):
return True
return False
def _is_batch_generated(obj):
return bool((getattr(obj, "QetBatchAssemblyKind", "") or "").strip())
def _existing_terminal_strip_devices(doc, strip_name=""):
wanted = (strip_name or "").strip().lower()
devices = []
for obj in list(getattr(doc, "Objects", []) or []):
if not _is_terminal_strip_device(obj):
continue
current_strip, order = _parse_strip_name_and_order(obj)
if wanted and current_strip.lower() != wanted:
continue
devices.append((current_strip, order, obj))
devices.sort(
key=lambda item: (
item[0].lower(),
item[1] if item[1] is not None else 10**9,
_natural_sort_key(getattr(item[2], "Label", "") or getattr(item[2], "Name", "")),
)
)
return [obj for _strip, _order, obj in devices]
def available_terminal_strip_names(doc):
names = []
seen = set()
for obj in list(getattr(doc, "Objects", []) or []):
if not _is_terminal_strip_device(obj):
continue
strip_name, _order = _parse_strip_name_and_order(obj)
key = strip_name.lower()
if strip_name and key not in seen:
seen.add(key)
names.append(strip_name)
names.sort(key=_natural_sort_key)
return names
def _existing_devices_by_prefix(doc, prefix=""):
devices = []
for obj in list(getattr(doc, "Objects", []) or []):
if not _is_qet_device_object(obj):
continue
if _is_terminal_strip_device(obj):
continue
if _is_batch_generated(obj):
continue
if not _matches_prefix(obj, prefix):
continue
devices.append(obj)
devices.sort(key=lambda obj: _natural_sort_key(getattr(obj, "Label", "") or getattr(obj, "Name", "")))
return devices
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 _vector_copy(vector):
return App.Vector(
float(getattr(vector, "x", 0.0) or 0.0),
float(getattr(vector, "y", 0.0) or 0.0),
float(getattr(vector, "z", 0.0) or 0.0),
)
def _vector_add(left, right):
return App.Vector(
float(getattr(left, "x", 0.0) or 0.0) + float(getattr(right, "x", 0.0) or 0.0),
float(getattr(left, "y", 0.0) or 0.0) + float(getattr(right, "y", 0.0) or 0.0),
float(getattr(left, "z", 0.0) or 0.0) + float(getattr(right, "z", 0.0) or 0.0),
)
def _vector_sub(left, right):
return App.Vector(
float(getattr(left, "x", 0.0) or 0.0) - float(getattr(right, "x", 0.0) or 0.0),
float(getattr(left, "y", 0.0) or 0.0) - float(getattr(right, "y", 0.0) or 0.0),
float(getattr(left, "z", 0.0) or 0.0) - float(getattr(right, "z", 0.0) or 0.0),
)
def _placement_base(obj):
placement = getattr(obj, "Placement", None)
base = getattr(placement, "Base", None)
if base is None:
return None
return _vector_copy(base)
def _placement_controls_children(obj):
try:
return bool(obj is not None and obj.isDerivedFrom("App::Part"))
except Exception:
return False
def _iter_transform_children(obj):
for child in list(getattr(obj, "Group", []) or []):
yield child
if not _placement_controls_children(child):
for nested in _iter_transform_children(child):
yield nested
def _object_anchor_point(obj):
if obj is None:
return App.Vector(0, 0, 0)
children = list(getattr(obj, "Group", []) or [])
if _placement_controls_children(obj) or not children:
return _placement_base(obj) or App.Vector(0, 0, 0)
points = []
for child in _iter_transform_children(obj):
base = _placement_base(child)
if base is not None:
points.append(base)
if not points:
return _placement_base(obj) or App.Vector(0, 0, 0)
return App.Vector(
sum(point.x for point in points) / len(points),
sum(point.y for point in points) / len(points),
sum(point.z for point in points) / len(points),
)
def _translate_placement(obj, delta):
placement = getattr(obj, "Placement", None)
base = getattr(placement, "Base", None)
if placement is None or base is None:
return False
new_base = _vector_add(base, delta)
try:
placement.Base = new_base
obj.Placement = placement
return True
except Exception:
try:
obj.Placement = App.Placement(new_base, getattr(placement, "Rotation", App.Rotation()))
return True
except Exception:
return False
def _translate_group_children(obj, delta):
moved = 0
for child in list(getattr(obj, "Group", []) or []):
if _translate_placement(child, delta):
moved += 1
if not _placement_controls_children(child):
moved += _translate_group_children(child, delta)
return moved
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 "",
)
TerminalObjects.ensure_string_property(
obj,
"QetMountHostKind",
"QET Mount",
"Mount host kind",
(getattr(host, "QetCarrierKind", "") or "").strip(),
)
def _set_layout_properties(obj, kind, batch_name, host, order_index, offset_mm):
_set_batch_properties(obj, kind, batch_name, host)
TerminalObjects.ensure_string_property(
obj,
"QetBatchAssemblyMode",
"QET Batch Assembly",
"Batch assembly mode",
"layout_existing",
)
TerminalObjects.ensure_string_property(
obj,
"QetBatchAssemblyOrder",
"QET Batch Assembly",
"Batch assembly order",
str(int(order_index)),
)
TerminalObjects.ensure_string_property(
obj,
"QetBatchAssemblyOffsetMm",
"QET Batch Assembly",
"Batch assembly offset in millimeters",
"{0:.6f}".format(float(offset_mm or 0.0)),
)
def _set_object_placement(obj, placement):
if obj is not None and getattr(obj, "Group", None) and not _placement_controls_children(obj):
current = _object_anchor_point(obj)
target = getattr(placement, "Base", App.Vector(0, 0, 0))
delta = _vector_sub(target, current)
moved_children = _translate_group_children(obj, delta)
try:
obj.Placement = placement
except Exception:
pass
if moved_children:
return True
try:
obj.Placement = placement
return True
except Exception:
try:
existing = getattr(obj, "Placement", None)
if existing is not None:
existing.Base = placement.Base
existing.Rotation = placement.Rotation
obj.Placement = existing
return True
except Exception:
pass
return False
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 _terminal_objects_for_devices(devices):
terminals = []
for device in devices or []:
try:
terminals.extend(TerminalObjects.collect_terminal_objects(device))
except Exception:
pass
return terminals
def _batch_report(kind, group, devices, terminals, source="fallback_created"):
return {
"kind": kind,
"group": group,
"devices": devices,
"terminals": terminals,
"created_devices": len(devices),
"created_terminals": len(terminals),
"updated_devices": 0,
"updated_terminals": 0,
"source": source,
}
def _layout_existing_objects(
doc,
rail,
objects,
kind,
batch_name,
pitch_mm,
start_offset_mm,
):
rail = _ensure_rail(rail)
objects = [obj for obj in objects or [] if obj is not None]
if not objects:
return _batch_report(kind, rail, [], [], source="qet_existing")
base = _base_point(rail)
axis = _axis_vector(rail)
updated = []
for index, obj in enumerate(objects):
offset = float(start_offset_mm or 0.0) + index * float(pitch_mm or 0.0)
placement = _placement_at(rail, _point_at(base, axis, offset))
if _set_object_placement(obj, placement):
_set_layout_properties(obj, kind, batch_name, rail, index + 1, offset)
updated.append(obj)
try:
doc.recompute()
except Exception:
pass
terminals = _terminal_objects_for_devices(updated)
return {
"kind": kind,
"group": rail,
"devices": updated,
"terminals": terminals,
"created_devices": 0,
"created_terminals": 0,
"updated_devices": len(updated),
"updated_terminals": len(terminals),
"source": "qet_existing",
}
def layout_existing_terminal_block(
doc,
rail,
block_name="",
pitch_mm=5.2,
start_offset_mm=0.0,
):
if doc is None:
raise BatchAssemblyError("请先打开 FreeCAD 工程。")
rail = _ensure_rail(rail)
devices = _existing_terminal_strip_devices(doc, block_name)
if not devices:
return _batch_report("terminal_block", rail, [], [], source="qet_existing")
batch_name = _safe_label(block_name, _parse_strip_name_and_order(devices[0])[0] or "QET端子排")
return _layout_existing_objects(
doc,
rail,
devices,
"terminal_block",
batch_name,
pitch_mm,
start_offset_mm,
)
def layout_existing_devices(
doc,
rail,
prefix="QF",
pitch_mm=18.0,
start_offset_mm=0.0,
kind="device_batch",
):
if doc is None:
raise BatchAssemblyError("请先打开 FreeCAD 工程。")
rail = _ensure_rail(rail)
devices = _existing_devices_by_prefix(doc, prefix)
if not devices:
return _batch_report(kind, rail, [], [], source="qet_existing")
batch_name = _safe_label(prefix, "QET设备")
return _layout_existing_objects(
doc,
rail,
devices,
kind,
batch_name,
pitch_mm,
start_offset_mm,
)
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)