feature/FreeCAD手动布线与回写-zwl-0525
parent
ad3e17234a
commit
076f20b2e8
@ -0,0 +1,402 @@
|
|||||||
|
# FreeCADExchange wiring object helpers.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
import FreeCAD as App
|
||||||
|
|
||||||
|
import TerminalObjects
|
||||||
|
|
||||||
|
|
||||||
|
WIRING_GROUP_NAME = "QETWiring"
|
||||||
|
WIRING_GROUP_LABEL = "QET Wiring"
|
||||||
|
WIRING_GROUP_KIND = "Wiring"
|
||||||
|
WIRING_BUCKET_KIND = "WiringBucket"
|
||||||
|
WIRE_TASK_KIND = "WireTask"
|
||||||
|
ROUTED_WIRE_KIND = "RoutedWire"
|
||||||
|
|
||||||
|
WIRING_BUCKETS = [
|
||||||
|
("QETWiring_01_Tasks", "01_Tasks"),
|
||||||
|
("QETWiring_02_Carriers", "02_Carriers"),
|
||||||
|
("QETWiring_03_Previews", "03_Previews"),
|
||||||
|
("QETWiring_04_Routed", "04_Routed"),
|
||||||
|
("QETWiring_05_Diagnostics", "05_Diagnostics"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _set_string_property(obj, prop_name, value, description="QET wiring property"):
|
||||||
|
TerminalObjects.ensure_string_property(
|
||||||
|
obj,
|
||||||
|
prop_name,
|
||||||
|
"QET Wiring",
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_bool_property(obj, prop_name, value, description="QET wiring property"):
|
||||||
|
TerminalObjects.ensure_bool_property(
|
||||||
|
obj,
|
||||||
|
prop_name,
|
||||||
|
"QET Wiring",
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_to_parent(parent, child):
|
||||||
|
if parent is not None and child not in getattr(parent, "Group", []):
|
||||||
|
parent.addObject(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_group(doc, name, label, kind, parent=None, project_uuid=""):
|
||||||
|
group = doc.getObject(name)
|
||||||
|
if group is None:
|
||||||
|
group = doc.addObject("App::DocumentObjectGroup", name)
|
||||||
|
|
||||||
|
_add_to_parent(parent, group)
|
||||||
|
group.Label = label
|
||||||
|
_set_string_property(group, "QetGroupKind", kind, "FreeCADExchange group kind")
|
||||||
|
_set_string_property(group, "QetProjectUuid", project_uuid, "Project UUID for wiring")
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_wiring_root_group(doc, project_uuid=""):
|
||||||
|
root = TerminalObjects.ensure_root_group(doc, project_uuid)
|
||||||
|
wiring_root = _ensure_group(
|
||||||
|
doc,
|
||||||
|
WIRING_GROUP_NAME,
|
||||||
|
WIRING_GROUP_LABEL,
|
||||||
|
WIRING_GROUP_KIND,
|
||||||
|
parent=root,
|
||||||
|
project_uuid=project_uuid,
|
||||||
|
)
|
||||||
|
ensure_wiring_buckets(doc, wiring_root, project_uuid=project_uuid)
|
||||||
|
return wiring_root
|
||||||
|
|
||||||
|
|
||||||
|
def _hide_object(obj):
|
||||||
|
try:
|
||||||
|
obj.ViewObject.Visibility = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _is_legacy_wire_group(obj):
|
||||||
|
if obj is None:
|
||||||
|
return False
|
||||||
|
if getattr(obj, "Name", "").startswith(TerminalObjects.WIRE_GROUP_PREFIX):
|
||||||
|
return True
|
||||||
|
return (getattr(obj, "QetGroupKind", "") or "").strip() == TerminalObjects.WIRE_GROUP_KIND
|
||||||
|
|
||||||
|
|
||||||
|
def hide_legacy_wire_groups(doc):
|
||||||
|
hidden = 0
|
||||||
|
for obj in list(getattr(doc, "Objects", []) or []):
|
||||||
|
if not _is_legacy_wire_group(obj):
|
||||||
|
continue
|
||||||
|
_hide_object(obj)
|
||||||
|
hidden += 1
|
||||||
|
return hidden
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_wiring_scene(doc, project_uuid=""):
|
||||||
|
wiring_root = ensure_wiring_root_group(doc, project_uuid)
|
||||||
|
hide_legacy_wire_groups(doc)
|
||||||
|
return wiring_root
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_wiring_buckets(doc, wiring_root, project_uuid=""):
|
||||||
|
buckets = {}
|
||||||
|
for name, label in WIRING_BUCKETS:
|
||||||
|
buckets[label] = _ensure_group(
|
||||||
|
doc,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
WIRING_BUCKET_KIND,
|
||||||
|
parent=wiring_root,
|
||||||
|
project_uuid=project_uuid,
|
||||||
|
)
|
||||||
|
return buckets
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_bucket_group(doc, bucket_label, project_uuid=""):
|
||||||
|
wiring_root = ensure_wiring_root_group(doc, project_uuid)
|
||||||
|
for name, label in WIRING_BUCKETS:
|
||||||
|
if label == bucket_label:
|
||||||
|
return _ensure_group(
|
||||||
|
doc,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
WIRING_BUCKET_KIND,
|
||||||
|
parent=wiring_root,
|
||||||
|
project_uuid=project_uuid,
|
||||||
|
)
|
||||||
|
raise ValueError("Unknown wiring bucket: {0}".format(bucket_label))
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_task_group(doc, project_uuid=""):
|
||||||
|
return ensure_bucket_group(doc, "01_Tasks", project_uuid=project_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_carrier_group(doc, project_uuid=""):
|
||||||
|
return ensure_bucket_group(doc, "02_Carriers", project_uuid=project_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_preview_group(doc, project_uuid=""):
|
||||||
|
return ensure_bucket_group(doc, "03_Previews", project_uuid=project_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_routed_group(doc, project_uuid=""):
|
||||||
|
return ensure_bucket_group(doc, "04_Routed", project_uuid=project_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_diagnostic_group(doc, project_uuid=""):
|
||||||
|
return ensure_bucket_group(doc, "05_Diagnostics", project_uuid=project_uuid)
|
||||||
|
|
||||||
|
|
||||||
|
def _wire_safe_name(prefix, wire_uuid, fallback="Wire"):
|
||||||
|
token = TerminalObjects.safe_token((wire_uuid or "").strip())
|
||||||
|
return "{0}_{1}".format(prefix, token or fallback)
|
||||||
|
|
||||||
|
|
||||||
|
def _wire_common_properties(
|
||||||
|
obj,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
):
|
||||||
|
_set_string_property(obj, "QetProjectUuid", project_uuid, "Project UUID")
|
||||||
|
_set_string_property(obj, "QetWireUuid", wire_uuid, "Wire UUID")
|
||||||
|
_set_string_property(obj, "QetWireLabel", wire_label, "Wire label")
|
||||||
|
_set_string_property(obj, "QetStartTerminalUuid", start_terminal_uuid, "Start terminal UUID")
|
||||||
|
_set_string_property(obj, "QetEndTerminalUuid", end_terminal_uuid, "End terminal UUID")
|
||||||
|
_set_string_property(obj, "QetStartInstanceId", start_instance_id, "Start instance ID")
|
||||||
|
_set_string_property(obj, "QetEndInstanceId", end_instance_id, "End instance ID")
|
||||||
|
|
||||||
|
|
||||||
|
def set_wire_task_semantics(
|
||||||
|
obj,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
net_uuid="",
|
||||||
|
group_uuid="",
|
||||||
|
wire_mark="",
|
||||||
|
wire_mark_is_manual=False,
|
||||||
|
):
|
||||||
|
_wire_common_properties(
|
||||||
|
obj,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
)
|
||||||
|
_set_string_property(obj, "QetNetUuid", net_uuid, "Net UUID")
|
||||||
|
_set_string_property(obj, "QetGroupUuid", group_uuid, "Group UUID")
|
||||||
|
_set_string_property(obj, "QetWireMark", wire_mark, "Wire mark")
|
||||||
|
_set_bool_property(obj, "QetWireMarkIsManual", wire_mark_is_manual, "Whether wire mark is manual")
|
||||||
|
_set_string_property(obj, "RouteType", "Task", "Wire task type")
|
||||||
|
_set_string_property(obj, "RouteStatus", "Task", "Wire task status")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def set_routed_wire_semantics(
|
||||||
|
obj,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
route_type="Manual",
|
||||||
|
route_status="Routed",
|
||||||
|
route_mode="Manual",
|
||||||
|
net_uuid="",
|
||||||
|
group_uuid="",
|
||||||
|
wire_mark="",
|
||||||
|
wire_mark_is_manual=False,
|
||||||
|
):
|
||||||
|
_wire_common_properties(
|
||||||
|
obj,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
)
|
||||||
|
_set_string_property(obj, "QetNetUuid", net_uuid, "Net UUID")
|
||||||
|
_set_string_property(obj, "QetGroupUuid", group_uuid, "Group UUID")
|
||||||
|
_set_string_property(obj, "QetWireMark", wire_mark, "Wire mark")
|
||||||
|
_set_bool_property(obj, "QetWireMarkIsManual", wire_mark_is_manual, "Whether wire mark is manual")
|
||||||
|
_set_string_property(obj, "RouteType", route_type, "Wire route type")
|
||||||
|
_set_string_property(obj, "RouteStatus", route_status, "Wire route status")
|
||||||
|
_set_string_property(obj, "RouteMode", route_mode, "Wire route mode")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def create_wire_task(
|
||||||
|
doc,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
net_uuid="",
|
||||||
|
group_uuid="",
|
||||||
|
wire_mark="",
|
||||||
|
wire_mark_is_manual=False,
|
||||||
|
):
|
||||||
|
task_name = _wire_safe_name("QETWireTask", wire_uuid)
|
||||||
|
task = doc.addObject("App::DocumentObjectGroup", task_name)
|
||||||
|
task.Label = wire_label or wire_uuid or "QET Wire Task"
|
||||||
|
set_wire_task_semantics(
|
||||||
|
task,
|
||||||
|
project_uuid,
|
||||||
|
wire_uuid,
|
||||||
|
wire_label,
|
||||||
|
start_terminal_uuid,
|
||||||
|
end_terminal_uuid,
|
||||||
|
start_instance_id,
|
||||||
|
end_instance_id,
|
||||||
|
net_uuid=net_uuid,
|
||||||
|
group_uuid=group_uuid,
|
||||||
|
wire_mark=wire_mark,
|
||||||
|
wire_mark_is_manual=wire_mark_is_manual,
|
||||||
|
)
|
||||||
|
_add_to_parent(ensure_task_group(doc, project_uuid), task)
|
||||||
|
return task
|
||||||
|
|
||||||
|
|
||||||
|
def _point_from_vector(vector):
|
||||||
|
return {
|
||||||
|
"x": float(getattr(vector, "x", 0.0)),
|
||||||
|
"y": float(getattr(vector, "y", 0.0)),
|
||||||
|
"z": float(getattr(vector, "z", 0.0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def wire_shape_points(wire_obj):
|
||||||
|
if wire_obj is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
points = list(getattr(wire_obj, "Points", []) or [])
|
||||||
|
if points:
|
||||||
|
return points
|
||||||
|
|
||||||
|
shape = getattr(wire_obj, "Shape", None)
|
||||||
|
if isinstance(shape, (list, tuple)):
|
||||||
|
return [
|
||||||
|
point
|
||||||
|
for point in shape
|
||||||
|
if hasattr(point, "x") and hasattr(point, "y") and hasattr(point, "z")
|
||||||
|
]
|
||||||
|
|
||||||
|
vertexes = getattr(shape, "Vertexes", None)
|
||||||
|
if vertexes:
|
||||||
|
result = []
|
||||||
|
for vertex in vertexes:
|
||||||
|
point = getattr(vertex, "Point", None)
|
||||||
|
if point is not None:
|
||||||
|
result.append(point)
|
||||||
|
return result
|
||||||
|
|
||||||
|
ordered_vertexes = getattr(shape, "OrderedVertexes", None)
|
||||||
|
if ordered_vertexes:
|
||||||
|
result = []
|
||||||
|
for vertex in ordered_vertexes:
|
||||||
|
point = getattr(vertex, "Point", None)
|
||||||
|
if point is not None:
|
||||||
|
result.append(point)
|
||||||
|
return result
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def wire_payload_from_object(wire_obj):
|
||||||
|
start_terminal_uuid = getattr(wire_obj, "QetStartTerminalUuid", "").strip()
|
||||||
|
end_terminal_uuid = getattr(wire_obj, "QetEndTerminalUuid", "").strip()
|
||||||
|
if TerminalObjects.is_local_terminal_uuid(start_terminal_uuid) or TerminalObjects.is_local_terminal_uuid(end_terminal_uuid):
|
||||||
|
return {
|
||||||
|
"wire_uuid": getattr(wire_obj, "QetWireUuid", "").strip(),
|
||||||
|
"wire_label": getattr(wire_obj, "QetWireLabel", "").strip(),
|
||||||
|
"route_type": getattr(wire_obj, "RouteType", "").strip(),
|
||||||
|
"route_status": getattr(wire_obj, "RouteStatus", "").strip(),
|
||||||
|
"route_mode": getattr(wire_obj, "RouteMode", "").strip(),
|
||||||
|
"start_terminal_uuid": "",
|
||||||
|
"end_terminal_uuid": "",
|
||||||
|
"start_instance_id": getattr(wire_obj, "QetStartInstanceId", "").strip(),
|
||||||
|
"end_instance_id": getattr(wire_obj, "QetEndInstanceId", "").strip(),
|
||||||
|
"net_uuid": getattr(wire_obj, "QetNetUuid", "").strip(),
|
||||||
|
"group_uuid": getattr(wire_obj, "QetGroupUuid", "").strip(),
|
||||||
|
"wire_mark": getattr(wire_obj, "QetWireMark", "").strip(),
|
||||||
|
"wire_mark_is_manual": bool(getattr(wire_obj, "QetWireMarkIsManual", False)),
|
||||||
|
"points": [],
|
||||||
|
}
|
||||||
|
points = [_point_from_vector(point) for point in wire_shape_points(wire_obj)]
|
||||||
|
return {
|
||||||
|
"wire_uuid": getattr(wire_obj, "QetWireUuid", "").strip(),
|
||||||
|
"wire_label": getattr(wire_obj, "QetWireLabel", "").strip(),
|
||||||
|
"route_type": getattr(wire_obj, "RouteType", "").strip(),
|
||||||
|
"route_status": getattr(wire_obj, "RouteStatus", "").strip(),
|
||||||
|
"route_mode": getattr(wire_obj, "RouteMode", "").strip(),
|
||||||
|
"start_terminal_uuid": start_terminal_uuid,
|
||||||
|
"end_terminal_uuid": end_terminal_uuid,
|
||||||
|
"start_instance_id": getattr(wire_obj, "QetStartInstanceId", "").strip(),
|
||||||
|
"end_instance_id": getattr(wire_obj, "QetEndInstanceId", "").strip(),
|
||||||
|
"net_uuid": getattr(wire_obj, "QetNetUuid", "").strip(),
|
||||||
|
"group_uuid": getattr(wire_obj, "QetGroupUuid", "").strip(),
|
||||||
|
"wire_mark": getattr(wire_obj, "QetWireMark", "").strip(),
|
||||||
|
"wire_mark_is_manual": bool(getattr(wire_obj, "QetWireMarkIsManual", False)),
|
||||||
|
"points": points,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_routed_wire_object(obj):
|
||||||
|
if obj is None:
|
||||||
|
return False
|
||||||
|
properties = getattr(obj, "PropertiesList", [])
|
||||||
|
return (
|
||||||
|
"QetStartTerminalUuid" in properties
|
||||||
|
and "QetEndTerminalUuid" in properties
|
||||||
|
and (getattr(obj, "RouteType", "") or "").strip() in {"Manual", "GuidedManual", "AutoSuggested"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_routed_wire_objects(doc):
|
||||||
|
root = doc.getObject(WIRING_GROUP_NAME)
|
||||||
|
if root is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
routed_group = None
|
||||||
|
for candidate in list(getattr(root, "Group", []) or []):
|
||||||
|
if getattr(candidate, "Name", "") == "QETWiring_04_Routed":
|
||||||
|
routed_group = candidate
|
||||||
|
break
|
||||||
|
if routed_group is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for obj in list(getattr(routed_group, "Group", []) or []):
|
||||||
|
if is_routed_wire_object(obj):
|
||||||
|
result.append(obj)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_routed_wires(doc):
|
||||||
|
return [wire_payload_from_object(obj) for obj in iter_routed_wire_objects(doc)]
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
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_modules():
|
||||||
|
fake_freecad = types.ModuleType("FreeCAD")
|
||||||
|
fake_freecad.ActiveDocument = object()
|
||||||
|
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
|
||||||
|
|
||||||
|
fake_gui = types.ModuleType("FreeCADGui")
|
||||||
|
fake_gui.getMainWindow = lambda: None
|
||||||
|
sys.modules["FreeCADGui"] = fake_gui
|
||||||
|
|
||||||
|
fake_device_import = types.ModuleType("DeviceImport")
|
||||||
|
fake_device_import.DeviceImportError = RuntimeError
|
||||||
|
fake_device_import.import_devices_from_payload = lambda *args, **kwargs: {}
|
||||||
|
sys.modules["DeviceImport"] = fake_device_import
|
||||||
|
|
||||||
|
fake_device_preview = types.ModuleType("DevicePreview")
|
||||||
|
fake_device_preview.find_parent_qet_device_object = lambda obj: None
|
||||||
|
fake_device_preview.is_preview_document_name = lambda name: False
|
||||||
|
fake_device_preview.open_preview_for_device_object = lambda obj: None
|
||||||
|
sys.modules["DevicePreview"] = fake_device_preview
|
||||||
|
|
||||||
|
fake_terminal_import = types.ModuleType("TerminalImport")
|
||||||
|
fake_terminal_import.TerminalImportError = RuntimeError
|
||||||
|
sys.modules["TerminalImport"] = fake_terminal_import
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
fake_wiring = types.ModuleType("WiringObjects")
|
||||||
|
fake_wiring.initialize_wiring_scene = lambda doc, project_uuid="": calls.append((doc, project_uuid)) or "root"
|
||||||
|
sys.modules["WiringObjects"] = fake_wiring
|
||||||
|
|
||||||
|
class _QObject:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
fake_qt_core = types.SimpleNamespace(
|
||||||
|
QObject=_QObject,
|
||||||
|
QEvent=types.SimpleNamespace(MouseButtonDblClick=1),
|
||||||
|
QTimer=types.SimpleNamespace(singleShot=lambda *args, **kwargs: None),
|
||||||
|
)
|
||||||
|
fake_qt_widgets = types.SimpleNamespace(
|
||||||
|
QWidget=object,
|
||||||
|
QMessageBox=types.SimpleNamespace(
|
||||||
|
information=lambda *args, **kwargs: None,
|
||||||
|
critical=lambda *args, **kwargs: None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
fake_pyside = types.ModuleType("PySide6")
|
||||||
|
fake_pyside.QtCore = fake_qt_core
|
||||||
|
fake_pyside.QtWidgets = fake_qt_widgets
|
||||||
|
sys.modules["PySide6"] = fake_pyside
|
||||||
|
|
||||||
|
return fake_freecad, calls
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeBootstrapWiringTest(unittest.TestCase):
|
||||||
|
def test_initialize_wiring_scene_uses_active_document_and_project_uuid(self):
|
||||||
|
app, calls = _install_fake_modules()
|
||||||
|
sys.modules.pop("ExchangeBootstrap", None)
|
||||||
|
bootstrap = importlib.import_module("ExchangeBootstrap")
|
||||||
|
|
||||||
|
result = bootstrap._initialize_wiring_scene({"project_uuid": "project-1"})
|
||||||
|
|
||||||
|
self.assertEqual("root", result)
|
||||||
|
self.assertEqual([(app.ActiveDocument, "project-1")], calls)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,336 @@
|
|||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
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": []}
|
||||||
|
fake_freecadgui = types.ModuleType("FreeCADGui")
|
||||||
|
fake_freecadgui.addCommand = lambda *args, **kwargs: None
|
||||||
|
fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None
|
||||||
|
fake_freecadgui.Selection = types.SimpleNamespace(
|
||||||
|
getSelection=lambda: list(selection_state["selection"]),
|
||||||
|
getSelectionEx=lambda: list(selection_state["selection_ex"]),
|
||||||
|
)
|
||||||
|
fake_freecadgui.Control = types.SimpleNamespace(
|
||||||
|
activeDialog=lambda: False,
|
||||||
|
showDialog=lambda panel: panel,
|
||||||
|
closeDialog=lambda: None,
|
||||||
|
)
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 _reload_modules():
|
||||||
|
for name in [
|
||||||
|
"TerminalObjects",
|
||||||
|
"WiringObjects",
|
||||||
|
"ManualWiring",
|
||||||
|
"TemplateAuthoring",
|
||||||
|
"ExchangeWriteBack",
|
||||||
|
"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_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_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(5, len(getattr(wire, "Points", [])))
|
||||||
|
self.assertEqual(
|
||||||
|
(1.0, 12.0, 3.0),
|
||||||
|
(
|
||||||
|
wire.Points[1].x,
|
||||||
|
wire.Points[1].y,
|
||||||
|
wire.Points[1].z,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual((10.0, 20.0, 30.0), (wire.Points[2].x, wire.Points[2].y, wire.Points[2].z))
|
||||||
|
self.assertEqual((9.0, 8.0, 17.0), (wire.Points[3].x, wire.Points[3].y, wire.Points[3].z))
|
||||||
|
self.assertEqual((9.0, 8.0, 7.0), (wire.Points[4].x, wire.Points[4].y, wire.Points[4].z))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,290 @@
|
|||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
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:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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.Console = types.SimpleNamespace(
|
||||||
|
PrintMessage=lambda *args, **kwargs: None,
|
||||||
|
PrintWarning=lambda *args, **kwargs: None,
|
||||||
|
PrintError=lambda *args, **kwargs: None,
|
||||||
|
)
|
||||||
|
fake_freecad.ActiveDocument = None
|
||||||
|
sys.modules["FreeCAD"] = fake_freecad
|
||||||
|
|
||||||
|
fake_freecadgui = types.ModuleType("FreeCADGui")
|
||||||
|
fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None
|
||||||
|
fake_freecadgui.addCommand = lambda *args, **kwargs: None
|
||||||
|
fake_freecadgui.Selection = types.SimpleNamespace(getSelection=lambda: [])
|
||||||
|
sys.modules["FreeCADGui"] = fake_freecadgui
|
||||||
|
|
||||||
|
fake_importgui = types.ModuleType("ImportGui")
|
||||||
|
fake_importgui.insert = lambda *args, **kwargs: None
|
||||||
|
sys.modules["ImportGui"] = fake_importgui
|
||||||
|
|
||||||
|
fake_device_preview = types.ModuleType("DevicePreview")
|
||||||
|
fake_device_preview.find_main_exchange_document = lambda *args, **kwargs: None
|
||||||
|
sys.modules["DevicePreview"] = fake_device_preview
|
||||||
|
|
||||||
|
|
||||||
|
class FakeViewObject:
|
||||||
|
def __init__(self):
|
||||||
|
self.Visibility = True
|
||||||
|
self.ShapeColor = None
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def isDerivedFrom(self, type_name):
|
||||||
|
if self.TypeId == type_name:
|
||||||
|
return True
|
||||||
|
if type_name == "App::DocumentObjectGroup":
|
||||||
|
return self.TypeId in {"App::DocumentObjectGroup", "App::Part"}
|
||||||
|
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.Name = "QETScene"
|
||||||
|
self.Objects = []
|
||||||
|
|
||||||
|
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 _reload_modules():
|
||||||
|
for name in [
|
||||||
|
"DeviceImport",
|
||||||
|
"TemplateSemantics",
|
||||||
|
"TerminalImport",
|
||||||
|
"TerminalObjects",
|
||||||
|
]:
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
terminal_import = importlib.import_module("TerminalImport")
|
||||||
|
terminal_objects = importlib.import_module("TerminalObjects")
|
||||||
|
device_import = importlib.import_module("DeviceImport")
|
||||||
|
return terminal_import, terminal_objects, device_import
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalImportTemplateSlotPolicyTest(unittest.TestCase):
|
||||||
|
def test_import_skips_terminal_when_device_has_no_template_slots(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_import, terminal_objects, device_import = _reload_modules()
|
||||||
|
|
||||||
|
doc = FakeDocument()
|
||||||
|
device_import._ensure_document = lambda scene_path: doc
|
||||||
|
root = device_import._ensure_root_group(doc, project_uuid="project-1")
|
||||||
|
device = doc.addObject("App::Part", "QETDevice_device_a")
|
||||||
|
root.addObject(device)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
device,
|
||||||
|
"QetProjectUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"Project UUID",
|
||||||
|
"project-1",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
device,
|
||||||
|
"QetElementUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"Element UUID",
|
||||||
|
"device-a",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
device,
|
||||||
|
"QetInstanceId",
|
||||||
|
"QET Exchange",
|
||||||
|
"Instance ID",
|
||||||
|
"instance-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
report = terminal_import.import_terminals_from_payload(
|
||||||
|
{
|
||||||
|
"project_uuid": "project-1",
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"element_uuid": "device-a",
|
||||||
|
"instance_id": "instance-a",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminals": [
|
||||||
|
{
|
||||||
|
"terminal_uuid": "terminal-a",
|
||||||
|
"element_uuid": "device-a",
|
||||||
|
"instance_id": "instance-a",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal_group = terminal_objects.find_child_group_by_kind(
|
||||||
|
device,
|
||||||
|
terminal_objects.TERMINAL_GROUP_KIND,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, report["imported_terminals"])
|
||||||
|
self.assertEqual(1, report.get("skipped_missing_slot"))
|
||||||
|
self.assertEqual([], terminal_objects.collect_terminal_objects(terminal_group))
|
||||||
|
|
||||||
|
def test_import_uses_slot_name_hint_to_match_template_slots(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_import, terminal_objects, device_import = _reload_modules()
|
||||||
|
app = sys.modules["FreeCAD"]
|
||||||
|
|
||||||
|
doc = FakeDocument()
|
||||||
|
device_import._ensure_document = lambda scene_path: doc
|
||||||
|
root = device_import._ensure_root_group(doc, project_uuid="project-1")
|
||||||
|
device = doc.addObject("App::Part", "QETDevice_device_a")
|
||||||
|
root.addObject(device)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
device,
|
||||||
|
"QetProjectUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"Project UUID",
|
||||||
|
"project-1",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
device,
|
||||||
|
"QetElementUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"Element UUID",
|
||||||
|
"device-a",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
device,
|
||||||
|
"QetInstanceId",
|
||||||
|
"QET Exchange",
|
||||||
|
"Instance ID",
|
||||||
|
"instance-a",
|
||||||
|
)
|
||||||
|
|
||||||
|
slot_p1 = doc.addObject("Part::LocalCoordinateSystem", "Terminal_P1")
|
||||||
|
slot_p1.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation())
|
||||||
|
slot_p1.addProperty("App::PropertyString", "Role", "QET Template", "")
|
||||||
|
slot_p1.Role = "Terminal"
|
||||||
|
slot_p1.addProperty("App::PropertyBool", "CanWire", "QET Template", "")
|
||||||
|
slot_p1.CanWire = True
|
||||||
|
slot_p1.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
|
||||||
|
slot_p1.QetTemplateSlotName = "P1"
|
||||||
|
slot_p1.addProperty("App::PropertyString", "QetTerminalLabel", "QET Template", "")
|
||||||
|
slot_p1.QetTerminalLabel = "P1"
|
||||||
|
device.addObject(slot_p1)
|
||||||
|
|
||||||
|
slot_p2 = doc.addObject("Part::LocalCoordinateSystem", "Terminal_P2")
|
||||||
|
slot_p2.Placement = app.Placement(app.Vector(20, 0, 0), app.Rotation())
|
||||||
|
slot_p2.addProperty("App::PropertyString", "Role", "QET Template", "")
|
||||||
|
slot_p2.Role = "Terminal"
|
||||||
|
slot_p2.addProperty("App::PropertyBool", "CanWire", "QET Template", "")
|
||||||
|
slot_p2.CanWire = True
|
||||||
|
slot_p2.addProperty("App::PropertyString", "QetTemplateSlotName", "QET Template", "")
|
||||||
|
slot_p2.QetTemplateSlotName = "P2"
|
||||||
|
slot_p2.addProperty("App::PropertyString", "QetTerminalLabel", "QET Template", "")
|
||||||
|
slot_p2.QetTerminalLabel = "P2"
|
||||||
|
device.addObject(slot_p2)
|
||||||
|
|
||||||
|
report = terminal_import.import_terminals_from_payload(
|
||||||
|
{
|
||||||
|
"project_uuid": "project-1",
|
||||||
|
"devices": [
|
||||||
|
{
|
||||||
|
"element_uuid": "device-a",
|
||||||
|
"instance_id": "instance-a",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"terminals": [
|
||||||
|
{
|
||||||
|
"terminal_uuid": "terminal-p2",
|
||||||
|
"element_uuid": "device-a",
|
||||||
|
"instance_id": "instance-a",
|
||||||
|
"slot_name_hint": "P2",
|
||||||
|
"terminal_label": "P2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"terminal_uuid": "terminal-p1",
|
||||||
|
"element_uuid": "device-a",
|
||||||
|
"instance_id": "instance-a",
|
||||||
|
"slot_name_hint": "P1",
|
||||||
|
"terminal_label": "P1",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal_group = terminal_objects.find_child_group_by_kind(
|
||||||
|
device,
|
||||||
|
terminal_objects.TERMINAL_GROUP_KIND,
|
||||||
|
)
|
||||||
|
terminals = {
|
||||||
|
getattr(obj, "QetTerminalUuid", ""): obj
|
||||||
|
for obj in terminal_objects.collect_terminal_objects(terminal_group)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(2, report["imported_terminals"])
|
||||||
|
self.assertEqual(2, len(terminals))
|
||||||
|
self.assertEqual(20.0, terminals["terminal-p2"].Placement.Base.x)
|
||||||
|
self.assertEqual(10.0, terminals["terminal-p1"].Placement.Base.x)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
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, transform=None):
|
||||||
|
self._transform = transform
|
||||||
|
|
||||||
|
def multVec(self, vector):
|
||||||
|
if self._transform is None:
|
||||||
|
return Vector(vector.x, vector.y, vector.z)
|
||||||
|
return self._transform(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
|
||||||
|
sys.modules["FreeCAD"] = fake_freecad
|
||||||
|
|
||||||
|
|
||||||
|
class FakeObject:
|
||||||
|
def __init__(self, name, type_id="App::DocumentObjectGroup"):
|
||||||
|
self.Name = name
|
||||||
|
self.TypeId = type_id
|
||||||
|
self.Group = []
|
||||||
|
self.InList = []
|
||||||
|
self.Placement = sys.modules["FreeCAD"].Placement()
|
||||||
|
self.PropertiesList = []
|
||||||
|
self.ViewObject = types.SimpleNamespace(Visibility=True)
|
||||||
|
|
||||||
|
def addObject(self, child):
|
||||||
|
if child not in self.Group:
|
||||||
|
self.Group.append(child)
|
||||||
|
if self not in child.InList:
|
||||||
|
child.InList.append(self)
|
||||||
|
|
||||||
|
def addProperty(self, prop_type, prop_name, group_name, description):
|
||||||
|
if prop_name not in self.PropertiesList:
|
||||||
|
self.PropertiesList.append(prop_name)
|
||||||
|
|
||||||
|
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 _reload_module():
|
||||||
|
sys.modules.pop("TerminalObjects", None)
|
||||||
|
return importlib.import_module("TerminalObjects")
|
||||||
|
|
||||||
|
|
||||||
|
class TerminalDirectionTest(unittest.TestCase):
|
||||||
|
def test_terminal_direction_uses_terminal_and_parent_rotation(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_objects = _reload_module()
|
||||||
|
app = sys.modules["FreeCAD"]
|
||||||
|
|
||||||
|
terminal_rotates_z_to_x = app.Rotation(
|
||||||
|
lambda vector: app.Vector(vector.z, vector.y, -vector.x)
|
||||||
|
)
|
||||||
|
parent_rotates_x_to_y = app.Rotation(
|
||||||
|
lambda vector: app.Vector(-vector.y, vector.x, vector.z)
|
||||||
|
)
|
||||||
|
parent = FakeObject("QETDevice_ct_1")
|
||||||
|
parent.Placement = app.Placement(app.Vector(100, 0, 0), parent_rotates_x_to_y)
|
||||||
|
terminal = FakeObject("Terminal_P1", "Part::LocalCoordinateSystem")
|
||||||
|
terminal.Placement = app.Placement(app.Vector(10, 0, 0), terminal_rotates_z_to_x)
|
||||||
|
parent.addObject(terminal)
|
||||||
|
|
||||||
|
direction = terminal_objects.terminal_direction(terminal)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(0.0, direction.x)
|
||||||
|
self.assertAlmostEqual(1.0, direction.y)
|
||||||
|
self.assertAlmostEqual(0.0, direction.z)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateTerminalVisibilityTest(unittest.TestCase):
|
||||||
|
def test_hide_template_terminal_hints_hides_template_lcs_but_keeps_engineering_terminal_visible(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_objects = _reload_module()
|
||||||
|
|
||||||
|
container = FakeObject("QETDevice_ct_1")
|
||||||
|
|
||||||
|
template_terminal = FakeObject("D1", "Part::LocalCoordinateSystem")
|
||||||
|
template_terminal.Role = "Terminal"
|
||||||
|
template_terminal.CanWire = True
|
||||||
|
template_terminal.PropertiesList = ["Role", "CanWire", "QetTemplateSlotName"]
|
||||||
|
container.addObject(template_terminal)
|
||||||
|
|
||||||
|
engineering_terminal = FakeObject("QETTerminal_D1", "Part::LocalCoordinateSystem")
|
||||||
|
terminal_objects.set_terminal_semantics(
|
||||||
|
engineering_terminal,
|
||||||
|
"project-1",
|
||||||
|
"device-1",
|
||||||
|
"terminal-1",
|
||||||
|
"instance-1",
|
||||||
|
label="D1",
|
||||||
|
slot_name="D1",
|
||||||
|
)
|
||||||
|
container.addObject(engineering_terminal)
|
||||||
|
|
||||||
|
hidden = terminal_objects.hide_template_terminal_hints(container)
|
||||||
|
|
||||||
|
self.assertEqual(1, hidden)
|
||||||
|
self.assertFalse(template_terminal.ViewObject.Visibility)
|
||||||
|
self.assertTrue(engineering_terminal.ViewObject.Visibility)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,354 @@
|
|||||||
|
import sys
|
||||||
|
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 vector.z == 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
|
||||||
|
|
||||||
|
fake_freecadgui = types.ModuleType("FreeCADGui")
|
||||||
|
fake_freecadgui.addCommand = lambda *args, **kwargs: None
|
||||||
|
fake_freecadgui.SendMsgToActiveView = lambda *args, **kwargs: None
|
||||||
|
fake_freecadgui.Selection = types.SimpleNamespace(
|
||||||
|
getSelection=lambda: [],
|
||||||
|
getSelectionEx=lambda: [],
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
class FakeDraftWire:
|
||||||
|
def __init__(self, obj):
|
||||||
|
obj.addProperty("App::PropertyVectorList", "Points", "Draft", "Wire points")
|
||||||
|
|
||||||
|
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)
|
||||||
|
obj.Closed = bool(closed)
|
||||||
|
obj.AttachmentSupport = support
|
||||||
|
obj.Placement = placement or fake_freecad.Placement()
|
||||||
|
FakeDraftWire(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
fake_draft.make_wire = make_wire
|
||||||
|
sys.modules["Draft"] = fake_draft
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 _reload_modules():
|
||||||
|
for name in ["TerminalObjects", "WiringObjects", "ManualWiring", "ExchangeWriteBack"]:
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
import TerminalObjects
|
||||||
|
import WiringObjects
|
||||||
|
import ManualWiring
|
||||||
|
import ExchangeWriteBack
|
||||||
|
return TerminalObjects, WiringObjects, ManualWiring, ExchangeWriteBack
|
||||||
|
|
||||||
|
|
||||||
|
class WiringTest(unittest.TestCase):
|
||||||
|
def test_ensure_wiring_root_group_creates_scene_buckets(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_objects, wiring_objects, _manual_wiring, _write_back = _reload_modules()
|
||||||
|
|
||||||
|
doc = FakeDocument()
|
||||||
|
root = terminal_objects.ensure_root_group(doc, "project-1")
|
||||||
|
wiring_root = wiring_objects.ensure_wiring_root_group(doc, "project-1")
|
||||||
|
|
||||||
|
self.assertIn(wiring_root, root.Group)
|
||||||
|
self.assertEqual("QETWiring", wiring_root.Name)
|
||||||
|
self.assertEqual("QET Wiring", wiring_root.Label)
|
||||||
|
self.assertIsNotNone(doc.getObject("QETWiring_01_Tasks"))
|
||||||
|
self.assertIsNotNone(doc.getObject("QETWiring_04_Routed"))
|
||||||
|
|
||||||
|
def test_initialize_wiring_scene_creates_root_and_hides_legacy_wire_groups(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_objects, wiring_objects, _manual_wiring, _write_back = _reload_modules()
|
||||||
|
|
||||||
|
doc = FakeDocument()
|
||||||
|
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")
|
||||||
|
|
||||||
|
legacy_group = terminal_objects.ensure_wire_group(
|
||||||
|
doc,
|
||||||
|
device,
|
||||||
|
project_uuid="project-1",
|
||||||
|
instance_id="instance-a",
|
||||||
|
)
|
||||||
|
legacy_group.ViewObject.Visibility = True
|
||||||
|
|
||||||
|
wiring_root = wiring_objects.initialize_wiring_scene(doc, "project-1")
|
||||||
|
|
||||||
|
self.assertEqual("QETWiring", wiring_root.Name)
|
||||||
|
self.assertIsNotNone(doc.getObject("QETWiring_04_Routed"))
|
||||||
|
self.assertFalse(legacy_group.ViewObject.Visibility)
|
||||||
|
|
||||||
|
def test_create_manual_wire_preserves_manual_waypoints_as_direct_segments(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_objects, wiring_objects, manual_wiring, _write_back = _reload_modules()
|
||||||
|
app = sys.modules["FreeCAD"]
|
||||||
|
|
||||||
|
doc = FakeDocument()
|
||||||
|
app.ActiveDocument = doc
|
||||||
|
root = terminal_objects.ensure_root_group(doc, "project-1")
|
||||||
|
wiring_objects.ensure_wiring_root_group(doc, "project-1")
|
||||||
|
|
||||||
|
start_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_start")
|
||||||
|
root.addObject(start_device)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
start_device,
|
||||||
|
"QetElementUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"",
|
||||||
|
"device-start",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
start_device,
|
||||||
|
"QetInstanceId",
|
||||||
|
"QET Exchange",
|
||||||
|
"",
|
||||||
|
"instance-start",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
start_device,
|
||||||
|
"QetProjectUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"",
|
||||||
|
"project-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
end_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_end")
|
||||||
|
root.addObject(end_device)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
end_device,
|
||||||
|
"QetElementUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"",
|
||||||
|
"device-end",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
end_device,
|
||||||
|
"QetInstanceId",
|
||||||
|
"QET Exchange",
|
||||||
|
"",
|
||||||
|
"instance-end",
|
||||||
|
)
|
||||||
|
terminal_objects.ensure_string_property(
|
||||||
|
end_device,
|
||||||
|
"QetProjectUuid",
|
||||||
|
"QET Exchange",
|
||||||
|
"",
|
||||||
|
"project-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
start_device.addObject(start_terminal)
|
||||||
|
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(9, 8, 7),
|
||||||
|
app.Rotation(w_axis=app.Vector(0, 0, 1)),
|
||||||
|
)
|
||||||
|
end_device.addObject(end_terminal)
|
||||||
|
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(4, 5, 6),
|
||||||
|
"support_axis": "x",
|
||||||
|
"anchor_kind": "face",
|
||||||
|
"source_label": "柜体面",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
terminal_exit_length=20.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
routed_group = doc.getObject("QETWiring_04_Routed")
|
||||||
|
self.assertIsNotNone(routed_group)
|
||||||
|
self.assertIn(wire, routed_group.Group)
|
||||||
|
self.assertEqual("Manual", getattr(wire, "RouteType", ""))
|
||||||
|
self.assertEqual("terminal-start", getattr(wire, "QetStartTerminalUuid", ""))
|
||||||
|
self.assertEqual("terminal-end", getattr(wire, "QetEndTerminalUuid", ""))
|
||||||
|
self.assertEqual(5, len(getattr(wire, "Points", [])))
|
||||||
|
self.assertEqual((1.0, 22.0, 3.0), (wire.Points[1].x, wire.Points[1].y, wire.Points[1].z))
|
||||||
|
self.assertTrue(any(point.x == 4.0 and point.y == 5.0 and point.z == 6.0 for point in wire.Points))
|
||||||
|
self.assertEqual((4.0, 5.0, 6.0), (wire.Points[2].x, wire.Points[2].y, wire.Points[2].z))
|
||||||
|
self.assertEqual((9.0, 8.0, 27.0), (wire.Points[3].x, wire.Points[3].y, wire.Points[3].z))
|
||||||
|
self.assertEqual((9.0, 8.0, 7.0), (wire.Points[4].x, wire.Points[4].y, wire.Points[4].z))
|
||||||
|
self.assertIn("QetManualWaypointsJson", getattr(wire, "PropertiesList", []))
|
||||||
|
self.assertIn('"support_axis": "x"', getattr(wire, "QetManualWaypointsJson", ""))
|
||||||
|
|
||||||
|
def test_wire_writeback_serializes_scene_routed_wire(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
terminal_objects, wiring_objects, manual_wiring, write_back = _reload_modules()
|
||||||
|
|
||||||
|
doc = FakeDocument()
|
||||||
|
root = terminal_objects.ensure_root_group(doc, "project-1")
|
||||||
|
wiring_objects.ensure_wiring_root_group(doc, "project-1")
|
||||||
|
routed_group = doc.getObject("QETWiring_04_Routed")
|
||||||
|
wire = doc.addObject("Part::Feature", "QETWire_terminal_start_terminal_end")
|
||||||
|
wire.Shape = [
|
||||||
|
sys.modules["FreeCAD"].Vector(1, 2, 3),
|
||||||
|
sys.modules["FreeCAD"].Vector(4, 5, 6),
|
||||||
|
]
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "terminal-start")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-end")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-start")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-end")
|
||||||
|
terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual")
|
||||||
|
routed_group.addObject(wire)
|
||||||
|
|
||||||
|
report = write_back.write_back_document(doc, scene_path=r"D:\tmp\scene.FCStd", payload={"project_uuid": "project-1"})
|
||||||
|
|
||||||
|
self.assertEqual(1, len(report["manual_wires"]))
|
||||||
|
self.assertEqual("terminal-start", report["manual_wires"][0]["start_terminal_uuid"])
|
||||||
|
self.assertEqual(2, len(report["manual_wires"][0]["points"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -0,0 +1,252 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
fake_freecad = types.ModuleType("FreeCAD")
|
||||||
|
fake_freecad.Vector = Vector
|
||||||
|
fake_freecad.ActiveDocument = None
|
||||||
|
fake_freecad.Console = types.SimpleNamespace(
|
||||||
|
PrintMessage=lambda *args, **kwargs: None,
|
||||||
|
PrintWarning=lambda *args, **kwargs: None,
|
||||||
|
PrintError=lambda *args, **kwargs: None,
|
||||||
|
)
|
||||||
|
fake_freecad.addDocumentObserver = lambda observer: None
|
||||||
|
sys.modules["FreeCAD"] = fake_freecad
|
||||||
|
|
||||||
|
fake_freecadgui = types.ModuleType("FreeCADGui")
|
||||||
|
fake_freecadgui.addCommand = lambda *args, **kwargs: None
|
||||||
|
sys.modules["FreeCADGui"] = fake_freecadgui
|
||||||
|
|
||||||
|
fake_importgui = types.ModuleType("ImportGui")
|
||||||
|
fake_importgui.insert = lambda *args, **kwargs: None
|
||||||
|
sys.modules["ImportGui"] = fake_importgui
|
||||||
|
|
||||||
|
fake_device_preview = types.ModuleType("DevicePreview")
|
||||||
|
fake_device_preview.find_main_exchange_document = lambda preferred_name="": None
|
||||||
|
sys.modules["DevicePreview"] = fake_device_preview
|
||||||
|
|
||||||
|
|
||||||
|
class FakeObject:
|
||||||
|
def __init__(self, name, type_id="App::DocumentObjectGroup"):
|
||||||
|
self.Name = name
|
||||||
|
self.Label = name
|
||||||
|
self.TypeId = type_id
|
||||||
|
self.PropertiesList = []
|
||||||
|
self.Group = []
|
||||||
|
self.InList = []
|
||||||
|
|
||||||
|
def isDerivedFrom(self, type_name):
|
||||||
|
if self.TypeId == type_name:
|
||||||
|
return True
|
||||||
|
if type_name == "App::DocumentObjectGroup":
|
||||||
|
return self.TypeId in {"App::DocumentObjectGroup", "App::Part"}
|
||||||
|
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.Name = "QETScene"
|
||||||
|
self.FileName = ""
|
||||||
|
self.Objects = []
|
||||||
|
|
||||||
|
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 _reload_writeback():
|
||||||
|
for name in ["DeviceImport", "TerminalObjects", "ExchangeWriteBack"]:
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
return importlib.import_module("ExchangeWriteBack"), importlib.import_module("TerminalObjects")
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeWriteBackManualWireTest(unittest.TestCase):
|
||||||
|
def test_write_back_skips_local_terminal_bindings(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
exchange_writeback, terminal_objects = _reload_writeback()
|
||||||
|
doc = FakeDocument()
|
||||||
|
|
||||||
|
root = terminal_objects.ensure_root_group(doc, "project-1")
|
||||||
|
device = doc.addObject("App::Part", "QETDevice_device_1")
|
||||||
|
root.addObject(device)
|
||||||
|
terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1")
|
||||||
|
terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1")
|
||||||
|
|
||||||
|
terminal_group = terminal_objects.ensure_terminal_group(
|
||||||
|
doc,
|
||||||
|
device,
|
||||||
|
project_uuid="project-1",
|
||||||
|
instance_id="instance-1",
|
||||||
|
)
|
||||||
|
qet_terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_real")
|
||||||
|
terminal_group.addObject(qet_terminal)
|
||||||
|
terminal_objects.set_terminal_semantics(
|
||||||
|
qet_terminal,
|
||||||
|
"project-1",
|
||||||
|
"device-1",
|
||||||
|
"terminal-real",
|
||||||
|
"instance-1",
|
||||||
|
)
|
||||||
|
local_terminal = doc.addObject("Part::LocalCoordinateSystem", "QETTerminal_local")
|
||||||
|
terminal_group.addObject(local_terminal)
|
||||||
|
terminal_objects.set_terminal_semantics(
|
||||||
|
local_terminal,
|
||||||
|
"project-1",
|
||||||
|
"device-1",
|
||||||
|
"local:instance-1:P1",
|
||||||
|
"instance-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
scene_path = str(Path(temp_dir) / "scene.FCStd")
|
||||||
|
report = exchange_writeback.write_back_document(
|
||||||
|
doc,
|
||||||
|
scene_path=scene_path,
|
||||||
|
payload={"project_uuid": "project-1"},
|
||||||
|
)
|
||||||
|
payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[{"terminal_uuid": "terminal-real", "instance_id": "instance-1"}],
|
||||||
|
report["terminals"],
|
||||||
|
)
|
||||||
|
self.assertEqual(report["terminals"], payload["terminals"])
|
||||||
|
|
||||||
|
def test_write_back_includes_manual_wires_with_route_points(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
exchange_writeback, terminal_objects = _reload_writeback()
|
||||||
|
app = sys.modules["FreeCAD"]
|
||||||
|
doc = FakeDocument()
|
||||||
|
|
||||||
|
root = terminal_objects.ensure_root_group(doc, "project-1")
|
||||||
|
device = doc.addObject("App::Part", "QETDevice_device_1")
|
||||||
|
root.addObject(device)
|
||||||
|
terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1")
|
||||||
|
terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1")
|
||||||
|
|
||||||
|
wire_group = terminal_objects.ensure_wire_group(
|
||||||
|
doc,
|
||||||
|
device,
|
||||||
|
project_uuid="project-1",
|
||||||
|
instance_id="instance-1",
|
||||||
|
)
|
||||||
|
wire = doc.addObject("Part::Feature", "QETWire_terminal_1_terminal_2")
|
||||||
|
wire_group.addObject(wire)
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "terminal-1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-2")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-2")
|
||||||
|
terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual")
|
||||||
|
wire.Shape = (
|
||||||
|
app.Vector(1, 2, 3),
|
||||||
|
app.Vector(4, 5, 6),
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
scene_path = str(Path(temp_dir) / "scene.FCStd")
|
||||||
|
report = exchange_writeback.write_back_document(
|
||||||
|
doc,
|
||||||
|
scene_path=scene_path,
|
||||||
|
payload={"project_uuid": "project-1"},
|
||||||
|
)
|
||||||
|
payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
self.assertEqual(1, len(report["manual_wires"]))
|
||||||
|
self.assertEqual(1, len(payload["manual_wires"]))
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"start_terminal_uuid": "terminal-1",
|
||||||
|
"end_terminal_uuid": "terminal-2",
|
||||||
|
"start_instance_id": "instance-1",
|
||||||
|
"end_instance_id": "instance-2",
|
||||||
|
"route_type": "Manual",
|
||||||
|
"points": [
|
||||||
|
{"x": 1.0, "y": 2.0, "z": 3.0},
|
||||||
|
{"x": 4.0, "y": 5.0, "z": 6.0},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
payload["manual_wires"][0],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_write_back_skips_manual_wires_with_local_terminal_endpoints(self):
|
||||||
|
_install_fake_freecad()
|
||||||
|
exchange_writeback, terminal_objects = _reload_writeback()
|
||||||
|
app = sys.modules["FreeCAD"]
|
||||||
|
doc = FakeDocument()
|
||||||
|
|
||||||
|
root = terminal_objects.ensure_root_group(doc, "project-1")
|
||||||
|
device = doc.addObject("App::Part", "QETDevice_device_1")
|
||||||
|
root.addObject(device)
|
||||||
|
terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", "device-1")
|
||||||
|
terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", "instance-1")
|
||||||
|
|
||||||
|
wire_group = terminal_objects.ensure_wire_group(
|
||||||
|
doc,
|
||||||
|
device,
|
||||||
|
project_uuid="project-1",
|
||||||
|
instance_id="instance-1",
|
||||||
|
)
|
||||||
|
wire = doc.addObject("Part::Feature", "QETWire_local_terminal")
|
||||||
|
wire_group.addObject(wire)
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetProjectUuid", "QET Exchange", "", "project-1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetStartTerminalUuid", "QET Exchange", "", "local:instance-1:P1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetEndTerminalUuid", "QET Exchange", "", "terminal-2")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetStartInstanceId", "QET Exchange", "", "instance-1")
|
||||||
|
terminal_objects.ensure_string_property(wire, "QetEndInstanceId", "QET Exchange", "", "instance-2")
|
||||||
|
terminal_objects.ensure_string_property(wire, "RouteType", "QET Exchange", "", "Manual")
|
||||||
|
wire.Shape = (
|
||||||
|
app.Vector(1, 2, 3),
|
||||||
|
app.Vector(4, 5, 6),
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
scene_path = str(Path(temp_dir) / "scene.FCStd")
|
||||||
|
report = exchange_writeback.write_back_document(
|
||||||
|
doc,
|
||||||
|
scene_path=scene_path,
|
||||||
|
payload={"project_uuid": "project-1"},
|
||||||
|
)
|
||||||
|
payload = json.loads(Path(report["output_path"]).read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
self.assertEqual([], report["manual_wires"])
|
||||||
|
self.assertEqual([], payload["manual_wires"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Loading…
Reference in New Issue