You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
LightWork3D/tests/python/freecad_exchange_manual_wir...

1105 lines
40 KiB
Python

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)
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"
self.transactions = []
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 openTransaction(self, name):
self.transactions.append(("open", name))
def commitTransaction(self):
self.transactions.append(("commit", ""))
def abortTransaction(self):
self.transactions.append(("abort", ""))
def undo(self):
self.transactions.append(("undo", ""))
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_builtin_carrier_asset_path_supports_installed_freecad_layout(self):
_selection_state = _install_fake_freecad()
_terminal_objects, panel = _reload_modules()
with tempfile.TemporaryDirectory() as temp_dir:
app_home = Path(temp_dir) / "run-FreeCAD"
module_dir = app_home / "Mod" / "FreeCADExchange"
asset_dir = app_home / "data" / "examples" / "qet_cabinet_assets"
module_dir.mkdir(parents=True)
asset_dir.mkdir(parents=True)
asset = asset_dir / "qet_din_rail.FCStd"
asset.write_text("fake rail", encoding="utf-8")
original_file = panel.__file__
try:
panel.__file__ = str(module_dir / "ManualWiringPanel.py")
resolved = panel._builtin_carrier_asset_path("rail")
finally:
panel.__file__ = original_file
self.assertEqual(str(asset), resolved)
def test_controller_rejects_local_terminal_as_manual_wiring_start(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
local_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalLocal")
terminal_objects.set_terminal_semantics(
local_terminal,
"project-1",
"device-a",
"local:instance-a:P1",
"instance-a",
label="P1",
)
selection_state["selection"] = [local_terminal]
with self.assertRaisesRegex(panel.ManualWiringPanelError, "QET 绑定工程端子"):
panel.ManualWiringController().set_start_from_selection()
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_records_selected_wire_duct_waypoint_as_carrier_anchor(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
carrier = doc.addObject("Part::Feature", "WireDuct_A")
carrier.Label = "线槽A"
terminal_objects.ensure_string_property(
carrier,
"QetCarrierKind",
"QET Wiring",
"Carrier kind",
"wire_duct",
)
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(100, 20, 30)],
SubObjects=[
types.SimpleNamespace(
ShapeType="Edge",
normalAt=lambda u: app.Vector(0, 1, 0),
)
],
SubElementNames=["Edge1"],
Object=carrier,
)
]
waypoint = panel.ManualWiringController().add_waypoint_from_selection()
self.assertEqual("edge", waypoint["anchor_kind"])
self.assertEqual("wire_duct", waypoint["carrier_kind"])
self.assertEqual("线槽A", waypoint["source_label"])
self.assertEqual("WireDuct_A", waypoint["source_object_name"])
def test_controller_records_wire_duct_edge_axis_for_waypoint(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
carrier = doc.addObject("Part::Feature", "WireDuct_A")
carrier.Label = "线槽A"
terminal_objects.ensure_string_property(
carrier,
"QetCarrierKind",
"QET Wiring",
"Carrier kind",
"wire_duct",
)
edge = types.SimpleNamespace(
ShapeType="Edge",
Vertexes=[
types.SimpleNamespace(Point=app.Vector(0, 10, 20)),
types.SimpleNamespace(Point=app.Vector(100, 10, 20)),
],
)
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(40, 10, 20)],
SubObjects=[edge],
SubElementNames=["Edge1"],
Object=carrier,
)
]
waypoint = panel.ManualWiringController().add_waypoint_from_selection()
self.assertEqual("wire_duct", waypoint["carrier_kind"])
self.assertEqual("x", waypoint["carrier_axis"])
def test_controller_marks_selected_object_as_wire_duct_carrier(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
carrier = doc.addObject("Part::Feature", "WireDuct_A")
carrier.Label = "线槽A"
selection_state["selection"] = [carrier]
marked = panel.ManualWiringController().mark_selected_carriers("wire_duct")
carrier_group = doc.getObject("QETWiring_02_Carriers")
self.assertEqual([carrier], marked)
self.assertEqual("wire_duct", getattr(carrier, "QetCarrierKind", ""))
self.assertEqual("线槽", getattr(carrier, "QetCarrierRoleLabel", ""))
self.assertIn(carrier, carrier_group.Group)
def test_controller_imports_wire_duct_carrier_from_asset_path(self):
_selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
def importer(doc, path):
obj = doc.addObject("Part::Feature", "ImportedWireDuct")
obj.Label = "Imported Wire Duct"
return [obj]
carrier = panel.ManualWiringController().import_carrier_asset(
r"D:\assets\duct.FCStd",
"wire_duct",
length_mm=600.0,
importer=importer,
)
carrier_group = doc.getObject("QETWiring_02_Carriers")
self.assertEqual("wire_duct", getattr(carrier, "QetCarrierKind", ""))
self.assertEqual("线槽", getattr(carrier, "QetCarrierRoleLabel", ""))
self.assertEqual(r"D:\assets\duct.FCStd", getattr(carrier, "QetCarrierSourcePath", ""))
self.assertEqual(200.0, getattr(carrier, "QetCarrierBaseLength", None))
self.assertEqual(600.0, getattr(carrier, "QetCarrierLength", None))
self.assertIn(carrier, carrier_group.Group)
self.assertEqual(3.0, getattr(carrier, "QetCarrierScaleX", None))
def test_controller_restores_active_document_after_carrier_import_changes_it(self):
_selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
def importer(doc, path):
obj = doc.addObject("Part::Feature", "ImportedRail")
app.ActiveDocument = None
return [obj]
panel.ManualWiringController().import_carrier_asset(
r"D:\assets\rail.FCStd",
"rail",
length_mm=300.0,
importer=importer,
)
self.assertIs(doc, app.ActiveDocument)
def test_controller_applies_length_to_selected_carrier(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
carrier = doc.addObject("App::DocumentObjectGroup", "Carrier")
terminal_objects.ensure_string_property(
carrier,
"QetCarrierKind",
"QET Wiring",
"Carrier kind",
"rail",
)
carrier.addProperty("App::PropertyFloat", "QetCarrierBaseLength", "QET Wiring", "Base length")
carrier.QetCarrierBaseLength = 200.0
selection_state["selection"] = [carrier]
updated = panel.ManualWiringController().apply_length_to_selected_carriers(500.0)
self.assertEqual([carrier], updated)
self.assertEqual(500.0, getattr(carrier, "QetCarrierLength", None))
self.assertEqual(2.5, getattr(carrier, "QetCarrierScaleX", None))
def test_controller_aligns_second_selected_face_to_first_selected_face(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
rail = doc.addObject("Part::Feature", "DINRail")
rail.Placement = app.Placement(app.Vector(0, 0, 10), app.Rotation())
target_face = types.SimpleNamespace(
ShapeType="Face",
normalAt=lambda u, v: app.Vector(0, 0, 1),
)
moving_face = types.SimpleNamespace(
ShapeType="Face",
normalAt=lambda u, v: app.Vector(0, 0, -1),
)
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(100, 20, 0)],
SubObjects=[target_face],
SubElementNames=["Face1"],
Object=cabinet,
),
types.SimpleNamespace(
PickedPoints=[app.Vector(5, 6, 9)],
SubObjects=[moving_face],
SubElementNames=["Face2"],
Object=rail,
),
]
result = panel.ManualWiringController().align_selected_contact_faces()
self.assertIs(rail, result["moving_object"])
self.assertEqual(
(0.0, 0.0, 1.0),
(rail.Placement.Base.x, rail.Placement.Base.y, rail.Placement.Base.z),
)
self.assertEqual(
(-0.0, -0.0, -9.0),
(
result["translation"].x,
result["translation"].y,
result["translation"].z,
),
)
self.assertEqual("normal", result["translation_mode"])
def test_controller_requires_two_faces_for_contact_alignment(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
face = types.SimpleNamespace(
ShapeType="Face",
normalAt=lambda u, v: app.Vector(0, 0, 1),
)
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(100, 20, 0)],
SubObjects=[face],
SubElementNames=["Face1"],
Object=cabinet,
)
]
with self.assertRaisesRegex(panel.ManualWiringPanelError, "目标面"):
panel.ManualWiringController().align_selected_contact_faces()
def test_controller_rejects_more_than_two_faces_for_contact_alignment(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
cabinet = doc.addObject("Part::Feature", "CabinetPanel")
rail = doc.addObject("Part::Feature", "DINRail")
breaker = doc.addObject("Part::Feature", "Breaker")
face = types.SimpleNamespace(
ShapeType="Face",
normalAt=lambda u, v: app.Vector(0, 0, 1),
)
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(0, 0, 0)],
SubObjects=[face],
SubElementNames=["Face1"],
Object=cabinet,
),
types.SimpleNamespace(
PickedPoints=[app.Vector(0, 0, 10)],
SubObjects=[face],
SubElementNames=["Face2"],
Object=rail,
),
types.SimpleNamespace(
PickedPoints=[app.Vector(0, 0, 20)],
SubObjects=[face],
SubElementNames=["Face3"],
Object=breaker,
),
]
with self.assertRaisesRegex(panel.ManualWiringPanelError, "只能选择两个面"):
panel.ManualWiringController().align_selected_contact_faces()
def test_controller_deletes_last_waypoint_and_preview_point(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
controller = panel.ManualWiringController(terminal_exit_length=10.0)
for point in [app.Vector(10, 20, 30), app.Vector(40, 50, 60)]:
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[point],
SubObjects=[],
SubElementNames=[],
Object=types.SimpleNamespace(Name="CabinetFace", Label="柜体面"),
)
]
controller.add_waypoint_from_selection()
preview_group = doc.getObject("QETWiring_03_Previews")
self.assertEqual(2, len(controller.waypoints))
self.assertEqual(2, len(controller.preview_objects))
self.assertEqual(2, len(preview_group.Group))
removed = controller.delete_last_waypoint()
self.assertIsNotNone(removed)
self.assertEqual(1, len(controller.waypoints))
self.assertEqual(1, len(controller.preview_objects))
self.assertEqual(1, len(preview_group.Group))
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(
[
(1.0, 2.0, 3.0),
(1.0, 12.0, 3.0),
(1.0, 12.0, 30.0),
(1.0, 20.0, 30.0),
(10.0, 20.0, 30.0),
(10.0, 20.0, 17.0),
(10.0, 8.0, 17.0),
(9.0, 8.0, 17.0),
(9.0, 8.0, 7.0),
],
[(point.x, point.y, point.z) for point in getattr(wire, "Points", [])],
)
def test_controller_diagnoses_last_generated_wire(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
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(20, 0, 0), app.Rotation())
terminal_objects.set_terminal_semantics(
end_terminal,
"project-1",
"device-end",
"terminal-end",
"instance-end",
label="End",
)
controller = panel.ManualWiringController()
selection_state["selection"] = [start_terminal]
controller.set_start_from_selection()
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(10, 10, 10)],
SubObjects=[],
SubElementNames=[],
Object=types.SimpleNamespace(Name="线槽无对象名", Label="线槽"),
)
]
controller.add_waypoint_from_selection()
controller.waypoints[0]["source_object_name"] = ""
selection_state["selection"] = [end_terminal]
controller.set_end_from_selection_and_generate()
diagnostics = controller.diagnose_last_wire()
self.assertTrue(
any(item["code"] == "wire_duct_source_missing" for item in diagnostics)
)
def test_controller_writes_all_wire_diagnostics(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
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(20, 0, 0), app.Rotation())
terminal_objects.set_terminal_semantics(
end_terminal,
"project-1",
"device-end",
"terminal-end",
"instance-end",
label="End",
)
controller = panel.ManualWiringController()
selection_state["selection"] = [start_terminal]
controller.set_start_from_selection()
controller.waypoints = [
{
"point": app.Vector(10, 10, 10),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
}
]
selection_state["selection"] = [end_terminal]
controller.set_end_from_selection_and_generate()
report = controller.diagnose_all_wires()
self.assertEqual(1, report["issue_count"])
self.assertEqual(1, len(doc.getObject("QETWiring_05_Diagnostics").Group))
def test_controller_generates_wire_from_selected_task(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
wiring_objects = importlib.import_module("WiringObjects")
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",
)
task = wiring_objects.create_wire_task(
doc,
"project-1",
"wire-1",
"W001",
"terminal-start",
"terminal-end",
"instance-a",
"instance-a",
net_uuid="net-1",
group_uuid="group-1",
wire_mark="W001",
wire_mark_is_manual=True,
)
controller = panel.ManualWiringController(terminal_exit_length=10.0)
controller.set_task_from_object(task)
selection_state["selection_ex"] = [
types.SimpleNamespace(
PickedPoints=[app.Vector(10, 20, 30)],
SubObjects=[],
SubElementNames=[],
Object=types.SimpleNamespace(Name="CabinetFace", Label="柜体面"),
)
]
controller.add_waypoint_from_selection()
selection_state["selection"] = []
wire = controller.set_end_from_selection_and_generate()
self.assertEqual("wire-1", getattr(wire, "QetWireUuid", ""))
self.assertEqual("net-1", getattr(wire, "QetNetUuid", ""))
self.assertEqual("group-1", getattr(wire, "QetGroupUuid", ""))
self.assertEqual("W001", getattr(wire, "QetWireMark", ""))
self.assertTrue(getattr(wire, "QetWireMarkIsManual", False))
self.assertEqual("terminal-start", getattr(wire, "QetStartTerminalUuid", ""))
self.assertEqual("terminal-end", getattr(wire, "QetEndTerminalUuid", ""))
self.assertEqual("Routed", getattr(task, "RouteStatus", ""))
def test_controller_prefers_task_element_uuid_when_terminal_uuid_is_reused(self):
_install_fake_freecad()
terminal_objects, panel = _reload_modules()
wiring_objects = importlib.import_module("WiringObjects")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
root = terminal_objects.ensure_root_group(doc, "project-1")
def add_device(element_uuid, instance_id, terminal_name):
device = doc.addObject("App::DocumentObjectGroup", "QETDevice_" + element_uuid)
root.addObject(device)
terminal_objects.ensure_string_property(device, "QetElementUuid", "QET Exchange", "", element_uuid)
terminal_objects.ensure_string_property(device, "QetInstanceId", "QET Exchange", "", instance_id)
terminal = doc.addObject("Part::LocalCoordinateSystem", terminal_name)
terminal.Placement = app.Placement(app.Vector(1, 2, 3), app.Rotation())
device.addObject(terminal)
terminal_objects.set_terminal_semantics(
terminal,
"project-1",
element_uuid,
"terminal-reused",
instance_id,
label=terminal_name,
)
return terminal
wrong_start = add_device("device-a", "instance-a", "WrongStart")
correct_start = add_device("device-b", "instance-b", "CorrectStart")
correct_end = add_device("device-c", "instance-c", "CorrectEnd")
_ = wrong_start
task = wiring_objects.create_wire_task(
doc,
"project-1",
"wire-1",
"W001",
"terminal-reused",
"terminal-reused",
"",
"",
)
terminal_objects.ensure_string_property(
task,
"QetStartElementUuid",
"QET Wiring",
"",
"device-b",
)
terminal_objects.ensure_string_property(
task,
"QetEndElementUuid",
"QET Wiring",
"",
"device-c",
)
controller = panel.ManualWiringController()
controller.set_task_from_object(task)
self.assertIs(correct_start, controller.start_terminal)
self.assertIs(correct_end, panel._find_terminal_by_uuid(doc, "terminal-reused", element_uuid="device-c"))
def test_controller_loads_selected_routed_wire_for_edit(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
manual_wiring = importlib.import_module("ManualWiring")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
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(100, 0, 0), app.Rotation())
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(40, 20, 0), "support_axis": "z"}],
terminal_exit_length=5.0,
wire_uuid="wire-1",
wire_label="W001",
)
selection_state["selection"] = [wire]
controller = panel.ManualWiringController()
loaded = controller.load_selected_wire_for_edit()
self.assertIs(wire, loaded)
self.assertIs(wire, controller.editing_wire)
self.assertIs(start_terminal, controller.start_terminal)
self.assertEqual(5.0, controller.terminal_exit_length)
self.assertEqual(1, len(controller.waypoints))
self.assertEqual(
(40.0, 20.0, 0.0),
(
controller.waypoints[0]["point"].x,
controller.waypoints[0]["point"].y,
controller.waypoints[0]["point"].z,
),
)
self.assertEqual(1, len(controller.preview_objects))
def test_controller_updates_loaded_wire_in_transaction(self):
selection_state = _install_fake_freecad()
terminal_objects, panel = _reload_modules()
manual_wiring = importlib.import_module("ManualWiring")
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
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(100, 0, 0), app.Rotation())
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(40, 20, 0)}],
wire_uuid="wire-1",
wire_label="W001",
)
selection_state["selection"] = [wire]
controller = panel.ManualWiringController()
controller.load_selected_wire_for_edit()
controller.waypoints = [{"point": app.Vector(70, 30, 10), "support_axis": "x"}]
updated = controller.update_loaded_wire()
routed_group = doc.getObject("QETWiring_04_Routed")
self.assertIs(wire, updated)
self.assertEqual([wire], routed_group.Group)
self.assertTrue(
any(point.x == 70.0 and point.y == 30.0 and point.z == 10.0 for point in wire.Points)
)
waypoints = json.loads(getattr(wire, "QetManualWaypointsJson", "[]"))
self.assertEqual("x", waypoints[0]["support_axis"])
self.assertEqual(
[("open", "修改手动导线"), ("commit", "")],
doc.transactions[-2:],
)
def test_controller_undo_last_change_uses_document_undo(self):
_install_fake_freecad()
terminal_objects, panel = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
app.ActiveDocument = doc
terminal_objects.ensure_root_group(doc, "project-1")
panel.ManualWiringController().undo_last_change()
self.assertEqual(("undo", ""), doc.transactions[-1])
if __name__ == "__main__":
unittest.main()