feature/FCStd设备模板端子制作-zwl-0520
parent
0866f0049c
commit
5fc2b9b04a
@ -0,0 +1,253 @@
|
||||
# FreeCADExchange FCStd template authoring helpers.
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
try:
|
||||
import FreeCADGui as Gui
|
||||
except ImportError:
|
||||
Gui = None
|
||||
|
||||
import TerminalObjects
|
||||
|
||||
|
||||
TEMPLATE_PROPERTY_GROUP = "QET Template"
|
||||
DEFAULT_TERMINAL_TYPE = "generic"
|
||||
|
||||
|
||||
class TemplateAuthoringError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def _safe_slot_name(slot_name):
|
||||
value = (slot_name or "").strip()
|
||||
if not value:
|
||||
raise TemplateAuthoringError("Terminal slot name is required.")
|
||||
return value
|
||||
|
||||
|
||||
def _terminal_object_name(slot_name):
|
||||
return "Terminal_{0}".format(TerminalObjects.safe_token(slot_name))
|
||||
|
||||
|
||||
def _ensure_template_property(obj, prop_name, value, prop_type="App::PropertyString"):
|
||||
if prop_type == "App::PropertyBool":
|
||||
TerminalObjects.ensure_bool_property(
|
||||
obj,
|
||||
prop_name,
|
||||
TEMPLATE_PROPERTY_GROUP,
|
||||
"QET template terminal property",
|
||||
bool(value),
|
||||
)
|
||||
else:
|
||||
TerminalObjects.ensure_string_property(
|
||||
obj,
|
||||
prop_name,
|
||||
TEMPLATE_PROPERTY_GROUP,
|
||||
"QET template terminal property",
|
||||
value,
|
||||
)
|
||||
|
||||
|
||||
def set_template_terminal_semantics(obj, slot_name, label="", terminal_type=DEFAULT_TERMINAL_TYPE):
|
||||
slot_name = _safe_slot_name(slot_name)
|
||||
label = (label or "").strip() or slot_name
|
||||
terminal_type = (terminal_type or "").strip() or DEFAULT_TERMINAL_TYPE
|
||||
|
||||
_ensure_template_property(obj, "Role", TerminalObjects.TERMINAL_ROLE)
|
||||
_ensure_template_property(obj, "CanWire", True, prop_type="App::PropertyBool")
|
||||
_ensure_template_property(obj, "QetTemplateSlotName", slot_name)
|
||||
_ensure_template_property(obj, "QetTerminalLabel", label)
|
||||
_ensure_template_property(obj, "QetTerminalType", terminal_type)
|
||||
obj.Label = label
|
||||
return obj
|
||||
|
||||
|
||||
def create_template_terminal(doc, slot_name, position, rotation=None, label="", terminal_type=DEFAULT_TERMINAL_TYPE):
|
||||
if doc is None:
|
||||
raise TemplateAuthoringError("An active FreeCAD document is required.")
|
||||
|
||||
slot_name = _safe_slot_name(slot_name)
|
||||
if position is None:
|
||||
raise TemplateAuthoringError("A terminal position is required.")
|
||||
|
||||
if rotation is None:
|
||||
rotation = App.Rotation()
|
||||
placement = App.Placement(position, rotation)
|
||||
terminal = TerminalObjects.create_lcs_object(
|
||||
doc,
|
||||
_terminal_object_name(slot_name),
|
||||
placement=placement,
|
||||
label=(label or slot_name),
|
||||
)
|
||||
set_template_terminal_semantics(
|
||||
terminal,
|
||||
slot_name,
|
||||
label=label or slot_name,
|
||||
terminal_type=terminal_type,
|
||||
)
|
||||
try:
|
||||
terminal.ViewObject.ShapeColor = (0.0, 0.75, 1.0)
|
||||
except Exception:
|
||||
pass
|
||||
doc.recompute()
|
||||
return terminal
|
||||
|
||||
|
||||
def is_template_terminal(obj):
|
||||
if obj is None:
|
||||
return False
|
||||
return TerminalObjects.is_terminal_hint_object(obj)
|
||||
|
||||
|
||||
def _has_property(obj, prop_name):
|
||||
return prop_name in getattr(obj, "PropertiesList", [])
|
||||
|
||||
|
||||
def validate_template_terminals(doc):
|
||||
report = {
|
||||
"document_name": getattr(doc, "Name", ""),
|
||||
"total_terminals": 0,
|
||||
"valid_terminals": 0,
|
||||
"warnings": [],
|
||||
"terminals": [],
|
||||
}
|
||||
if doc is None:
|
||||
report["warnings"].append("No active FreeCAD document.")
|
||||
return report
|
||||
|
||||
for obj in list(getattr(doc, "Objects", []) or []):
|
||||
if not is_template_terminal(obj):
|
||||
continue
|
||||
|
||||
report["total_terminals"] += 1
|
||||
slot_name = getattr(obj, "QetTemplateSlotName", "").strip()
|
||||
can_wire = bool(getattr(obj, "CanWire", False))
|
||||
item = {
|
||||
"name": getattr(obj, "Name", ""),
|
||||
"label": getattr(obj, "Label", ""),
|
||||
"slot_name": slot_name,
|
||||
"can_wire": can_wire,
|
||||
}
|
||||
report["terminals"].append(item)
|
||||
|
||||
valid = True
|
||||
if not _has_property(obj, "QetTemplateSlotName") or not slot_name:
|
||||
report["warnings"].append(
|
||||
"Template terminal {0} is missing QetTemplateSlotName.".format(
|
||||
getattr(obj, "Name", "")
|
||||
)
|
||||
)
|
||||
valid = False
|
||||
if not can_wire:
|
||||
report["warnings"].append(
|
||||
"Template terminal {0} has CanWire disabled.".format(
|
||||
getattr(obj, "Name", "")
|
||||
)
|
||||
)
|
||||
valid = False
|
||||
if valid:
|
||||
report["valid_terminals"] += 1
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def _selection_position():
|
||||
if Gui is None:
|
||||
return None
|
||||
try:
|
||||
selection_ex = Gui.Selection.getSelectionEx()
|
||||
except Exception:
|
||||
return None
|
||||
if not selection_ex:
|
||||
return None
|
||||
|
||||
picked = selection_ex[0]
|
||||
picked_points = list(getattr(picked, "PickedPoints", []) or [])
|
||||
if picked_points:
|
||||
return picked_points[0]
|
||||
|
||||
obj = getattr(picked, "Object", None)
|
||||
shape = getattr(obj, "Shape", None)
|
||||
bound_box = getattr(shape, "BoundBox", None)
|
||||
if bound_box is None:
|
||||
return None
|
||||
return App.Vector(
|
||||
(bound_box.XMin + bound_box.XMax) * 0.5,
|
||||
(bound_box.YMin + bound_box.YMax) * 0.5,
|
||||
(bound_box.ZMin + bound_box.ZMax) * 0.5,
|
||||
)
|
||||
|
||||
|
||||
class CommandAddTemplateTerminal:
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Add Template Terminal",
|
||||
"ToolTip": "Create a reusable electrical terminal LCS for an FCStd equipment template",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None and Gui is not None
|
||||
|
||||
def Activated(self):
|
||||
if Gui is None:
|
||||
return
|
||||
position = _selection_position()
|
||||
if position is None:
|
||||
App.Console.PrintWarning(
|
||||
"Select a model point or model object before adding a template terminal.\n"
|
||||
)
|
||||
return
|
||||
|
||||
slot_name = "T{0}".format(
|
||||
len(validate_template_terminals(App.ActiveDocument)["terminals"]) + 1
|
||||
)
|
||||
try:
|
||||
create_template_terminal(App.ActiveDocument, slot_name, position)
|
||||
App.Console.PrintMessage(
|
||||
"[FreeCADExchange] Created template terminal {0}. Rename QetTemplateSlotName if needed.\n".format(
|
||||
slot_name
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
App.Console.PrintError(
|
||||
"[FreeCADExchange] template terminal creation failed: {0}\n".format(exc)
|
||||
)
|
||||
|
||||
|
||||
class CommandValidateTemplateTerminals:
|
||||
def GetResources(self):
|
||||
return {
|
||||
"MenuText": "Validate Template Terminals",
|
||||
"ToolTip": "Validate electrical terminal LCS objects in the current FCStd template",
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
return App.ActiveDocument is not None
|
||||
|
||||
def Activated(self):
|
||||
report = validate_template_terminals(App.ActiveDocument)
|
||||
App.Console.PrintMessage(
|
||||
"[FreeCADExchange] Template terminals: {0} total, {1} valid\n".format(
|
||||
report["total_terminals"],
|
||||
report["valid_terminals"],
|
||||
)
|
||||
)
|
||||
for warning in report["warnings"]:
|
||||
App.Console.PrintWarning("[FreeCADExchange] {0}\n".format(warning))
|
||||
|
||||
|
||||
_COMMANDS_REGISTERED = False
|
||||
|
||||
|
||||
def register_commands():
|
||||
global _COMMANDS_REGISTERED
|
||||
if _COMMANDS_REGISTERED:
|
||||
return
|
||||
if Gui is None:
|
||||
return
|
||||
Gui.addCommand("QET_Template_AddTerminal", CommandAddTemplateTerminal())
|
||||
Gui.addCommand("QET_Template_ValidateTerminals", CommandValidateTemplateTerminals())
|
||||
_COMMANDS_REGISTERED = True
|
||||
|
||||
|
||||
register_commands()
|
||||
@ -0,0 +1,163 @@
|
||||
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):
|
||||
self.Axis = axis
|
||||
self.Angle = angle
|
||||
|
||||
class Placement:
|
||||
def __init__(self, base=None, rotation=None):
|
||||
self.Base = base
|
||||
self.Rotation = rotation
|
||||
|
||||
fake_freecad = types.ModuleType("FreeCAD")
|
||||
fake_freecad.Vector = Vector
|
||||
fake_freecad.Rotation = Rotation
|
||||
fake_freecad.Placement = Placement
|
||||
fake_freecad.ActiveDocument = None
|
||||
fake_freecad.Console = types.SimpleNamespace(
|
||||
PrintMessage=lambda *args, **kwargs: None,
|
||||
PrintWarning=lambda *args, **kwargs: None,
|
||||
PrintError=lambda *args, **kwargs: None,
|
||||
)
|
||||
sys.modules["FreeCAD"] = fake_freecad
|
||||
|
||||
fake_freecadgui = types.ModuleType("FreeCADGui")
|
||||
fake_freecadgui.addCommand = lambda *args, **kwargs: None
|
||||
fake_freecadgui.Selection = types.SimpleNamespace(getSelectionEx=lambda: [])
|
||||
sys.modules["FreeCADGui"] = fake_freecadgui
|
||||
|
||||
|
||||
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.ViewObject = FakeViewObject()
|
||||
self.Placement = None
|
||||
|
||||
def isDerivedFrom(self, type_name):
|
||||
if self.TypeId == type_name:
|
||||
return True
|
||||
if type_name == "App::DocumentObjectGroup":
|
||||
return self.TypeId == "App::DocumentObjectGroup"
|
||||
if type_name == "App::LocalCoordinateSystem":
|
||||
return self.TypeId in {
|
||||
"Part::LocalCoordinateSystem",
|
||||
"PartDesign::CoordinateSystem",
|
||||
}
|
||||
return False
|
||||
|
||||
def addProperty(self, prop_type, prop_name, group_name, description):
|
||||
if prop_name not in self.PropertiesList:
|
||||
self.PropertiesList.append(prop_name)
|
||||
|
||||
def addObject(self, child):
|
||||
if child not in self.Group:
|
||||
self.Group.append(child)
|
||||
|
||||
|
||||
class FakeDocument:
|
||||
def __init__(self):
|
||||
self.Name = "TemplateDoc"
|
||||
self.Objects = []
|
||||
self.recomputed = False
|
||||
|
||||
def addObject(self, type_name, name):
|
||||
obj = FakeObject(name, type_name)
|
||||
self.Objects.append(obj)
|
||||
return obj
|
||||
|
||||
def getObject(self, name):
|
||||
for obj in self.Objects:
|
||||
if obj.Name == name:
|
||||
return obj
|
||||
return None
|
||||
|
||||
def recompute(self):
|
||||
self.recomputed = True
|
||||
|
||||
|
||||
def _reload_modules():
|
||||
for name in ["TerminalObjects", "TemplateAuthoring"]:
|
||||
sys.modules.pop(name, None)
|
||||
return importlib.import_module("TemplateAuthoring")
|
||||
|
||||
|
||||
class TemplateAuthoringTest(unittest.TestCase):
|
||||
def test_create_template_terminal_writes_lcs_semantics(self):
|
||||
_install_fake_freecad()
|
||||
template_authoring = _reload_modules()
|
||||
app = sys.modules["FreeCAD"]
|
||||
doc = FakeDocument()
|
||||
|
||||
terminal = template_authoring.create_template_terminal(
|
||||
doc,
|
||||
"P1",
|
||||
app.Vector(10, 20, 30),
|
||||
terminal_type="primary",
|
||||
)
|
||||
|
||||
self.assertEqual("Terminal_P1", terminal.Name)
|
||||
self.assertEqual("P1", terminal.Label)
|
||||
self.assertEqual("Terminal", terminal.Role)
|
||||
self.assertTrue(terminal.CanWire)
|
||||
self.assertEqual("P1", terminal.QetTemplateSlotName)
|
||||
self.assertEqual("P1", terminal.QetTerminalLabel)
|
||||
self.assertEqual("primary", terminal.QetTerminalType)
|
||||
self.assertEqual(10.0, terminal.Placement.Base.x)
|
||||
self.assertEqual(20.0, terminal.Placement.Base.y)
|
||||
self.assertEqual(30.0, terminal.Placement.Base.z)
|
||||
self.assertTrue(doc.recomputed)
|
||||
|
||||
def test_validate_template_terminals_reports_missing_slot_name(self):
|
||||
_install_fake_freecad()
|
||||
template_authoring = _reload_modules()
|
||||
doc = FakeDocument()
|
||||
terminal = doc.addObject("Part::LocalCoordinateSystem", "BrokenTerminal")
|
||||
terminal.addProperty("App::PropertyString", "Role", "QET Template", "role")
|
||||
terminal.Role = "Terminal"
|
||||
terminal.addProperty(
|
||||
"App::PropertyBool",
|
||||
"CanWire",
|
||||
"QET Template",
|
||||
"can wire",
|
||||
)
|
||||
terminal.CanWire = True
|
||||
|
||||
report = template_authoring.validate_template_terminals(doc)
|
||||
|
||||
self.assertEqual(1, report["total_terminals"])
|
||||
self.assertEqual(0, report["valid_terminals"])
|
||||
self.assertEqual(1, len(report["warnings"]))
|
||||
self.assertIn("QetTemplateSlotName", report["warnings"][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in New Issue