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...

751 lines
25 KiB
Python

import sys
import types
import unittest
import json
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):
self.Axis = axis
self.Angle = angle
class Placement:
def __init__(self, base=None, rotation=None):
self.Base = base or Vector()
self.Rotation = 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.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_part = types.ModuleType("Part")
fake_part.makePolygon = lambda points: tuple(points)
sys.modules["Part"] = fake_part
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.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",
"DeviceImport",
"ManualWiring",
]:
sys.modules.pop(name, None)
import DeviceImport
import ManualWiring
import TerminalObjects
return DeviceImport, ManualWiring, TerminalObjects
class ManualWiringGroupTest(unittest.TestCase):
def test_manual_wire_uses_terminal_global_points_after_device_move(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
root = terminal_objects.ensure_root_group(doc, "project-1")
start_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_start")
start_device.Placement = app.Placement(app.Vector(100, 0, 0), app.Rotation())
root.addObject(start_device)
end_device = doc.addObject("App::DocumentObjectGroup", "QETDevice_end")
end_device.Placement = app.Placement(app.Vector(300, 0, 0), app.Rotation())
root.addObject(end_device)
for device, element_uuid, instance_id in [
(start_device, "device-start", "instance-start"),
(end_device, "device-end", "instance-end"),
]:
terminal_objects.ensure_string_property(
device,
"QetElementUuid",
"QET Exchange",
"Element UUID",
element_uuid,
)
terminal_objects.ensure_string_property(
device,
"QetInstanceId",
"QET Exchange",
"Instance ID",
instance_id,
)
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(10, 0, 0), app.Rotation())
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(20, 0, 0), app.Rotation())
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)
self.assertEqual(110.0, wire.Shape[0].x)
self.assertEqual(320.0, wire.Shape[-1].x)
def test_manual_wire_routes_orthogonally_between_terminal_exit_points(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
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(10, 20, 30), 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,
terminal_exit_length=10.0,
)
points = [(point.x, point.y, point.z) for point in wire.Shape]
self.assertEqual(
[
(0.0, 0.0, 0.0),
(0.0, 0.0, 10.0),
(0.0, 0.0, 40.0),
(0.0, 20.0, 40.0),
(10.0, 20.0, 40.0),
(10.0, 20.0, 30.0),
],
points,
)
def test_manual_wire_reaches_face_anchor_normal_axis_last(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
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, -100),
"support_axis": "z",
"anchor_kind": "face",
}
],
terminal_exit_length=20.0,
)
points = [(point.x, point.y, point.z) for point in wire.Shape]
self.assertEqual(
[
(0.0, 0.0, 0.0),
(0.0, 0.0, 20.0),
(40.0, 0.0, 20.0),
(40.0, 20.0, 20.0),
(40.0, 20.0, -100.0),
],
points[:5],
)
def test_manual_wire_records_semantic_route_nodes_for_later_carrier_routing(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
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, 20, 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(50, 10, 30),
"support_axis": "z",
"anchor_kind": "edge",
"carrier_kind": "wire_duct",
"source_label": "线槽A",
"subelement_name": "Edge1",
}
],
terminal_exit_length=15.0,
)
self.assertEqual(15.0, getattr(wire, "QetTerminalExitLength", None))
route_nodes = json.loads(getattr(wire, "QetRouteNodesJson", "[]"))
self.assertEqual(
[
"start_terminal",
"start_exit",
"waypoint",
"end_exit",
"end_terminal",
],
[node["role"] for node in route_nodes],
)
self.assertEqual("wire_duct", route_nodes[2]["carrier_kind"])
self.assertEqual("edge", route_nodes[2]["anchor_kind"])
self.assertEqual("terminal-start", route_nodes[0]["terminal_uuid"])
self.assertEqual("terminal-end", route_nodes[-1]["terminal_uuid"])
def test_manual_wire_routes_along_same_wire_duct_axis_between_waypoints(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
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(30, 120, 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(10, 0, 20),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
"source_object_name": "WireDuct_A",
},
{
"point": app.Vector(20, 100, 20),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
"source_object_name": "WireDuct_A",
},
],
terminal_exit_length=0.0,
)
points = [(point.x, point.y, point.z) for point in wire.Shape]
self.assertEqual(
[
(10.0, 0.0, 20.0),
(20.0, 0.0, 20.0),
(20.0, 100.0, 20.0),
],
points[2:5],
)
route_nodes = json.loads(getattr(wire, "QetRouteNodesJson", "[]"))
self.assertEqual("x", route_nodes[1]["carrier_axis"])
self.assertEqual("x", route_nodes[2]["carrier_axis"])
def test_manual_wire_does_not_treat_unknown_wire_duct_sources_as_same_carrier(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
terminal_objects.ensure_root_group(doc, "project-1")
start_terminal = doc.addObject("Part::LocalCoordinateSystem", "TerminalStart")
start_terminal.Placement = app.Placement(app.Vector(0, 0, 0), app.Rotation())
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(30, 120, 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(10, 0, 20),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
},
{
"point": app.Vector(20, 100, 20),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
},
],
terminal_exit_length=0.0,
)
points = [(point.x, point.y, point.z) for point in wire.Shape]
self.assertEqual((10.0, 100.0, 20.0), points[3])
def test_manual_wire_diagnostics_warn_for_wire_duct_waypoint_without_source(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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",
)
wire = manual_wiring.create_manual_wire(
doc,
start_terminal,
end_terminal,
waypoints=[
{
"point": app.Vector(10, 10, 10),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
}
],
)
diagnostics = manual_wiring.diagnose_manual_wire(wire)
self.assertTrue(
any(item["code"] == "wire_duct_source_missing" for item in diagnostics)
)
def test_manual_wire_diagnostics_pass_for_complete_manual_route(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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",
)
wire = manual_wiring.create_manual_wire(
doc,
start_terminal,
end_terminal,
waypoints=[
{
"point": app.Vector(10, 10, 10),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
"source_object_name": "WireDuct_A",
}
],
terminal_exit_length=20.0,
)
self.assertEqual([], manual_wiring.diagnose_manual_wire(wire))
def test_write_document_wire_diagnostics_creates_diagnostic_objects(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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",
)
wire = manual_wiring.create_manual_wire(
doc,
start_terminal,
end_terminal,
waypoints=[
{
"point": app.Vector(10, 10, 10),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
}
],
)
report = manual_wiring.write_document_wire_diagnostics(doc)
diagnostic_group = doc.getObject("QETWiring_05_Diagnostics")
self.assertEqual(1, report["issue_count"])
self.assertEqual(1, len(diagnostic_group.Group))
diagnostic = diagnostic_group.Group[0]
self.assertEqual("wire_duct_source_missing", diagnostic.QetDiagnosticCode)
self.assertEqual(wire.Name, diagnostic.QetWireObjectName)
self.assertIn("线槽折点", diagnostic.QetDiagnosticMessage)
def test_write_document_wire_diagnostics_replaces_previous_manual_results(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
app = sys.modules["FreeCAD"]
doc = FakeDocument()
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",
)
manual_wiring.create_manual_wire(
doc,
start_terminal,
end_terminal,
waypoints=[
{
"point": app.Vector(10, 10, 10),
"carrier_kind": "wire_duct",
"carrier_axis": "x",
}
],
)
manual_wiring.write_document_wire_diagnostics(doc)
manual_wiring.write_document_wire_diagnostics(doc)
diagnostic_group = doc.getObject("QETWiring_05_Diagnostics")
self.assertEqual(1, len(diagnostic_group.Group))
def test_manual_wire_is_visible_in_routed_group_not_hidden_legacy_group(self):
_install_fake_freecad()
_device_import, manual_wiring, terminal_objects = _reload_modules()
wiring_objects = __import__("WiringObjects")
doc = FakeDocument()
root = terminal_objects.ensure_root_group(doc, "project-1")
device_group = doc.addObject("App::DocumentObjectGroup", "QETDevice_device_a")
root.addObject(device_group)
terminal_objects.ensure_string_property(
device_group,
"QetElementUuid",
"QET Exchange",
"Element UUID",
"device-a",
)
terminal_objects.ensure_string_property(
device_group,
"QetInstanceId",
"QET Exchange",
"Instance ID",
"instance-a",
)
terminal_objects.ensure_string_property(
device_group,
"QetProjectUuid",
"QET Exchange",
"Project UUID",
"project-1",
)
start_terminal = FakeObject("TerminalStart", "Part::LocalCoordinateSystem")
terminal_objects.set_terminal_semantics(
start_terminal,
"project-1",
"device-a",
"terminal-start",
"instance-a",
label="Start",
)
end_terminal = FakeObject("TerminalEnd", "Part::LocalCoordinateSystem")
end_terminal.Placement = sys.modules["FreeCAD"].Placement(
sys.modules["FreeCAD"].Vector(10, 0, 0),
sys.modules["FreeCAD"].Rotation(),
)
terminal_objects.set_terminal_semantics(
end_terminal,
"project-1",
"device-a",
"terminal-end",
"instance-a",
label="End",
)
wire = manual_wiring.create_manual_wire(doc, start_terminal, end_terminal)
wire_group = terminal_objects.find_child_group_by_kind(
device_group,
terminal_objects.WIRE_GROUP_KIND,
)
routed_group = wiring_objects.ensure_routed_group(doc, "project-1")
self.assertIsNotNone(wire_group)
self.assertFalse(wire_group.ViewObject.Visibility)
self.assertIn(wire, routed_group.Group)
self.assertTrue(wire.ViewObject.Visibility)
self.assertNotIn(wire, wire_group.Group)
self.assertNotIn(wire, root.Group)
if __name__ == "__main__":
unittest.main()