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