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/docs/superpowers/plans/2026-05-20-freecad-fcstd-te...

591 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# FreeCAD FCStd Template Authoring Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a FreeCADExchange Python authoring tool that turns imported STEP/STP/STE geometry into reusable FCStd equipment templates with electrical terminal LCS semantics.
**Architecture:** Keep the feature in the FreeCADExchange Python layer. `TemplateAuthoring.py` owns template terminal creation, validation, and command registration; `TerminalObjects.py` remains the shared low-level semantic property helper; `InitGui.py` registers authoring commands when FreeCAD starts.
**Tech Stack:** FreeCAD Python API, `Part::LocalCoordinateSystem` / `PartDesign::CoordinateSystem`, Python `unittest`, existing FreeCADExchange command registration pattern.
---
## File Structure
- Create: `src/Mod/FreeCADExchange/TemplateAuthoring.py`
- Build template terminal LCS objects.
- Write template-only electrical semantics.
- Validate template terminal objects.
- Register FreeCAD commands.
- Modify: `src/Mod/FreeCADExchange/CMakeLists.txt`
- Add `TemplateAuthoring.py` to `FreeCADExchange_Scripts`.
- Modify: `src/Mod/FreeCADExchange/InitGui.py`
- Import `TemplateAuthoring`.
- Call `TemplateAuthoring.register_commands()`.
- Create: `tests/python/freecad_exchange_template_authoring_test.py`
- Fake enough FreeCAD API to test terminal creation and validation without launching FreeCAD.
- Modify: `docs/FreeCAD 端子显示连线保存回写开发文档.md`
- Add development result entry after implementation.
---
### Task 1: Add Failing Tests For Template Terminal Creation
**Files:**
- Create: `tests/python/freecad_exchange_template_authoring_test.py`
- [ ] **Step 1: Write the failing test file**
Create `tests/python/freecad_exchange_template_authoring_test.py` with:
```python
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)
import TemplateAuthoring
return 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()
```
- [ ] **Step 2: Run the test and verify RED**
Run:
```powershell
python -m unittest tests.python.freecad_exchange_template_authoring_test -v
```
Expected: fails with `ModuleNotFoundError: No module named 'TemplateAuthoring'`.
---
### Task 2: Implement TemplateAuthoring Core
**Files:**
- Create: `src/Mod/FreeCADExchange/TemplateAuthoring.py`
- [ ] **Step 1: Create the implementation**
Create `src/Mod/FreeCADExchange/TemplateAuthoring.py` with:
```python
# 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,
)
```
- [ ] **Step 2: Run the focused test and verify GREEN**
Run:
```powershell
python -m unittest tests.python.freecad_exchange_template_authoring_test -v
```
Expected: both tests pass.
---
### Task 3: Add FreeCAD Commands And Registration
**Files:**
- Modify: `src/Mod/FreeCADExchange/TemplateAuthoring.py`
- Modify: `src/Mod/FreeCADExchange/InitGui.py`
- Modify: `src/Mod/FreeCADExchange/CMakeLists.txt`
- [ ] **Step 1: Extend TemplateAuthoring.py with commands**
Append this code to `src/Mod/FreeCADExchange/TemplateAuthoring.py`:
```python
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()
```
- [ ] **Step 2: Modify InitGui.py**
Add the import near the existing FreeCADExchange imports:
```python
import TemplateAuthoring
```
Add registration after the existing command registration blocks:
```python
try:
TemplateAuthoring.register_commands()
except Exception:
pass
```
- [ ] **Step 3: Modify CMakeLists.txt**
Add `TemplateAuthoring.py` to `FreeCADExchange_Scripts`:
```cmake
TemplateAuthoring.py
```
Place it beside `TemplateSemantics.py`.
- [ ] **Step 4: Verify syntax and tests**
Run:
```powershell
python -m py_compile src/Mod/FreeCADExchange/TemplateAuthoring.py src/Mod/FreeCADExchange/InitGui.py
python -m unittest tests.python.freecad_exchange_template_authoring_test -v
```
Expected: py_compile exits 0, tests pass.
---
### Task 4: Add Development Result Documentation
**Files:**
- Modify: `docs/FreeCAD 端子显示连线保存回写开发文档.md`
- [ ] **Step 1: Add the result entry**
Append to the development log section:
```markdown
- 2026-05-20新增 FCStd 设备模板制作基础能力,支持把模型上的点位创建为带 `Role="Terminal"`、`CanWire=true`、`QetTemplateSlotName` 的模板端子 LCS已用单元测试验证端子语义写入和模板校验逻辑。
```
- [ ] **Step 2: Run final verification**
Run:
```powershell
python -m py_compile src/Mod/FreeCADExchange/TemplateAuthoring.py src/Mod/FreeCADExchange/InitGui.py src/Mod/FreeCADExchange/TerminalObjects.py
python -m unittest tests.python.freecad_exchange_template_authoring_test tests.python.freecad_exchange_template_semantics_test tests.python.freecad_exchange_manual_wiring_test -v
```
Expected: all tests pass.
- [ ] **Step 3: Commit**
Run:
```powershell
git add src/Mod/FreeCADExchange/TemplateAuthoring.py src/Mod/FreeCADExchange/InitGui.py src/Mod/FreeCADExchange/CMakeLists.txt tests/python/freecad_exchange_template_authoring_test.py "docs/FreeCAD 端子显示连线保存回写开发文档.md" "docs/FreeCAD 二次开发说明.md" docs/superpowers/specs/2026-05-20-freecad-fcstd-template-authoring-design.md docs/superpowers/plans/2026-05-20-freecad-fcstd-template-authoring.md
git commit -m "feature/FCStd设备模板端子制作-zwl-0520"
```
Expected: commit succeeds and does not include `artifacts/`, `video_frames/`, or generated cache files.
---
## Self-Review
Spec coverage:
- FCStd as formal asset is covered by Tasks 2 and 4.
- Template-only terminal semantics are covered by Task 1 and Task 2.
- Command entry points are covered by Task 3.
- Existing project import flow remains unchanged.
Placeholder scan:
- The plan contains no unresolved placeholders or open-ended implementation steps.
Type consistency:
- The planned module exposes `create_template_terminal`, `set_template_terminal_semantics`, `validate_template_terminals`, and `register_commands`.
- Command names are `QET_Template_AddTerminal` and `QET_Template_ValidateTerminals`.