feat: add qet terminal block model asset

dev
Zhaowenlong 4 weeks ago
parent 600ae67c0b
commit 6dfe26312a

@ -0,0 +1,45 @@
# QET Terminal Slice Model
This directory contains a reusable single-slice DIN rail terminal model for QET / FreeCAD exchange tests and future terminal block arrays.
## Files
- `qet_terminal_slice.FCStd`: formal FreeCAD template asset. It contains geometry plus template terminal LCS objects.
- `qet_terminal_slice.step`: geometry-only exchange export for external CAD preview or reuse.
- `qet_terminal_slice_report.json`: generated verification metadata.
- `create_qet_terminal_slice.py`: FreeCAD Python generator used to recreate the assets.
## Geometry
The slice uses a common modular terminal proportion:
- Width: `5.2 mm`
- Depth: `42.0 mm`
- Height: `36.0 mm`
Use `5.2 mm` spacing along the X axis when copying slices into a terminal block.
## Template Terminals
The FCStd template contains two terminal LCS objects:
- `Terminal_Top`, with `QetTemplateSlotName = "Top"`
- `Terminal_Bottom`, with `QetTemplateSlotName = "Bottom"`
Both objects set:
- `Role = "Terminal"`
- `CanWire = true`
- `QetTerminalType = "generic"`
The template intentionally does not store engineering binding fields such as `QetProjectUuid`, `QetElementUuid`, `QetTerminalUuid`, or `QetInstanceId`.
## Regenerate
On this Windows workstation, use the registered FreeCAD runtime:
```powershell
$runtime = Get-Content -LiteralPath 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' -Raw | ConvertFrom-Json
$env:QET_FREECAD_RUNTIME_JSON = 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json'
& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_terminal_block\create_qet_terminal_slice.py'
```

@ -0,0 +1,332 @@
# Generate a reusable QET DIN rail terminal slice FreeCAD template.
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
def _bootstrap_windows_freecad_runtime():
if os.name != "nt":
return
runtime_json = os.environ.get("QET_FREECAD_RUNTIME_JSON")
if not runtime_json:
local_app_data = os.environ.get("LOCALAPPDATA", "")
runtime_json = os.path.join(local_app_data, "QETDeps", "runtime.json")
if not runtime_json or not os.path.exists(runtime_json):
return
with open(runtime_json, "r", encoding="utf-8-sig") as handle:
runtime = json.load(handle)
roots = [str(item) for item in runtime.get("path_prefix", []) if item]
freecad_root = runtime.get("freecad_root", "")
if freecad_root:
roots.extend(
[
os.path.join(freecad_root, "build", "Mod", "Material"),
os.path.join(freecad_root, "build", "Mod", "Part"),
os.path.join(freecad_root, "build", "Mod", "Import"),
os.path.join(freecad_root, "build", "Mod"),
]
)
roots.append(os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "downlevel"))
for root in roots:
if root and os.path.isdir(root):
try:
os.add_dll_directory(root)
except (AttributeError, OSError):
pass
if root not in sys.path:
sys.path.append(root)
_bootstrap_windows_freecad_runtime()
import FreeCAD as App
import Part
WIDTH = 5.2
DEPTH = 42.0
HEIGHT = 36.0
OUT_DIR = Path(__file__).resolve().parent
FCSTD_PATH = OUT_DIR / "qet_terminal_slice.FCStd"
STEP_PATH = OUT_DIR / "qet_terminal_slice.step"
REPORT_PATH = OUT_DIR / "qet_terminal_slice_report.json"
ENGINEERING_BINDING_PROPERTIES = {
"QetProjectUuid",
"QetElementUuid",
"QetTerminalUuid",
"QetInstanceId",
}
def _box(name, dx, dy, dz, x, y, z, color, transparency=0):
obj = DOC.addObject("Part::Feature", name)
obj.Shape = Part.makeBox(dx, dy, dz, App.Vector(x, y, z))
_style(obj, color, transparency)
return obj
def _cylinder_y(name, radius, length, x, y, z, color):
obj = DOC.addObject("Part::Feature", name)
obj.Shape = Part.makeCylinder(
radius,
length,
App.Vector(x, y, z),
App.Vector(0, -1, 0),
)
_style(obj, color, 0)
return obj
def _style(obj, color, transparency=0):
if not hasattr(obj, "ViewObject") or obj.ViewObject is None:
return
obj.ViewObject.ShapeColor = color
obj.ViewObject.Transparency = transparency
def _ensure_property(obj, type_name, prop_name, group, description):
if prop_name not in obj.PropertiesList:
obj.addProperty(type_name, prop_name, group, description)
def _create_lcs(name, slot_name, label, base):
try:
lcs = DOC.addObject("Part::LocalCoordinateSystem", name)
except Exception:
lcs = DOC.addObject("PartDesign::CoordinateSystem", name)
rotation = App.Rotation(App.Vector(0, 0, 1), App.Vector(0, -1, 0))
lcs.Placement = App.Placement(base, rotation)
_ensure_property(lcs, "App::PropertyString", "Role", "QET Template", "QET object role")
_ensure_property(lcs, "App::PropertyBool", "CanWire", "QET Template", "Whether wires may connect")
_ensure_property(
lcs,
"App::PropertyString",
"QetTemplateSlotName",
"QET Template",
"Reusable template terminal slot name",
)
_ensure_property(
lcs,
"App::PropertyString",
"QetTerminalLabel",
"QET Template",
"Visible terminal label",
)
_ensure_property(
lcs,
"App::PropertyString",
"QetTerminalType",
"QET Template",
"Terminal type",
)
lcs.Role = "Terminal"
lcs.CanWire = True
lcs.QetTemplateSlotName = slot_name
lcs.QetTerminalLabel = label
lcs.QetTerminalType = "generic"
if hasattr(lcs, "ViewObject") and lcs.ViewObject is not None:
lcs.ViewObject.Visibility = True
return lcs
def _create_body():
x0 = -WIDTH / 2.0
y0 = -DEPTH / 2.0
base = Part.makeBox(WIDTH, DEPTH, HEIGHT, App.Vector(x0, y0, 0))
din_slot = Part.makeBox(
WIDTH + 1.0,
8.0,
10.0,
App.Vector(x0 - 0.5, DEPTH / 2.0 - 8.0, 12.0),
)
top_relief = Part.makeBox(
WIDTH + 1.0,
2.2,
7.0,
App.Vector(x0 - 0.5, y0 - 0.1, 26.0),
)
bottom_relief = Part.makeBox(
WIDTH + 1.0,
2.2,
7.0,
App.Vector(x0 - 0.5, y0 - 0.1, 3.0),
)
body = DOC.addObject("Part::Feature", "TerminalSlice_GreenBody")
body.Shape = base.cut(din_slot).cut(top_relief).cut(bottom_relief)
_style(body, (0.1, 0.72, 0.32), 18)
return body
def _create_visual_details():
x0 = -WIDTH / 2.0
y_front = -DEPTH / 2.0 - 0.9
y_back = DEPTH / 2.0
objects = [
_box(
"TerminalSlice_TopWhiteClamp",
WIDTH - 0.5,
1.2,
7.0,
x0 + 0.25,
y_front,
26.0,
(0.92, 0.94, 0.92),
0,
),
_box(
"TerminalSlice_BottomBlackClamp",
WIDTH - 0.5,
1.2,
7.0,
x0 + 0.25,
y_front,
3.0,
(0.02, 0.02, 0.02),
0,
),
_box(
"TerminalSlice_GreenWindow",
WIDTH - 0.8,
1.0,
8.0,
x0 + 0.4,
y_front - 0.02,
14.0,
(0.55, 0.95, 0.48),
42,
),
_box(
"TerminalSlice_DINRailTopLip",
WIDTH,
2.0,
2.0,
x0,
y_back - 1.8,
23.0,
(0.25, 0.31, 0.22),
0,
),
_box(
"TerminalSlice_DINRailBottomLip",
WIDTH,
2.0,
2.0,
x0,
y_back - 1.8,
10.0,
(0.25, 0.31, 0.22),
0,
),
_cylinder_y(
"TerminalSlice_TopScrew",
0.92,
0.7,
0.0,
y_front - 0.05,
29.5,
(0.35, 0.35, 0.35),
),
_cylinder_y(
"TerminalSlice_BottomScrew",
0.92,
0.7,
0.0,
y_front - 0.05,
6.5,
(0.0, 0.0, 0.0),
),
]
return objects
def _export_step(objects):
try:
import Import
Import.export(objects, str(STEP_PATH))
except Exception:
import ImportGui
ImportGui.export(objects, str(STEP_PATH))
def _write_report(terminals, geometry_objects):
report = {
"dimensions_mm": {
"width": WIDTH,
"depth": DEPTH,
"height": HEIGHT,
"array_spacing_x": WIDTH,
},
"outputs": {
"fcstd": str(FCSTD_PATH),
"step": str(STEP_PATH),
},
"geometry_objects": [obj.Name for obj in geometry_objects],
"terminals": [],
}
for terminal in terminals:
report["terminals"].append(
{
"name": terminal.Name,
"role": terminal.Role,
"can_wire": bool(terminal.CanWire),
"slot_name": terminal.QetTemplateSlotName,
"label": terminal.QetTerminalLabel,
"type": terminal.QetTerminalType,
"base": [
terminal.Placement.Base.x,
terminal.Placement.Base.y,
terminal.Placement.Base.z,
],
"engineering_binding_properties_present": [
prop
for prop in sorted(ENGINEERING_BINDING_PROPERTIES)
if prop in terminal.PropertiesList
],
}
)
REPORT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
DOC = App.newDocument("QETTerminalSlice")
body = _create_body()
details = _create_visual_details()
geometry = [body] + details
terminals = [
_create_lcs("Terminal_Top", "Top", "Top", App.Vector(0.0, -DEPTH / 2.0 - 1.0, 29.5)),
_create_lcs(
"Terminal_Bottom",
"Bottom",
"Bottom",
App.Vector(0.0, -DEPTH / 2.0 - 1.0, 6.5),
),
]
DOC.recompute()
DOC.saveAs(str(FCSTD_PATH))
_export_step(geometry)
_write_report(terminals, geometry)
print("Generated FCStd: {0}".format(FCSTD_PATH))
print("Generated STEP: {0}".format(STEP_PATH))
print("Generated report: {0}".format(REPORT_PATH))

File diff suppressed because it is too large Load Diff

@ -0,0 +1,52 @@
{
"dimensions_mm": {
"width": 5.2,
"depth": 42.0,
"height": 36.0,
"array_spacing_x": 5.2
},
"outputs": {
"fcstd": "D:\\LightWork3D\\data\\examples\\qet_terminal_block\\qet_terminal_slice.FCStd",
"step": "D:\\LightWork3D\\data\\examples\\qet_terminal_block\\qet_terminal_slice.step"
},
"geometry_objects": [
"TerminalSlice_GreenBody",
"TerminalSlice_TopWhiteClamp",
"TerminalSlice_BottomBlackClamp",
"TerminalSlice_GreenWindow",
"TerminalSlice_DINRailTopLip",
"TerminalSlice_DINRailBottomLip",
"TerminalSlice_TopScrew",
"TerminalSlice_BottomScrew"
],
"terminals": [
{
"name": "Terminal_Top",
"role": "Terminal",
"can_wire": true,
"slot_name": "Top",
"label": "Top",
"type": "generic",
"base": [
0.0,
-22.0,
29.5
],
"engineering_binding_properties_present": []
},
{
"name": "Terminal_Bottom",
"role": "Terminal",
"can_wire": true,
"slot_name": "Bottom",
"label": "Bottom",
"type": "generic",
"base": [
0.0,
-22.0,
6.5
],
"engineering_binding_properties_present": []
}
]
}

@ -0,0 +1,134 @@
# DIN Rail Terminal Slice Model 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:** Generate a reusable single-slice DIN rail terminal model as both `.FCStd` and `.step`, with FreeCAD template terminal LCS semantics.
**Architecture:** Add a focused FreeCAD Python generator under the asset directory. The script creates simple parametric solid geometry, adds two template terminal LCS objects with the existing QET terminal semantic properties, saves an FCStd template, exports STEP geometry, and writes a small metadata report for verification.
**Tech Stack:** FreeCADCmd, FreeCAD Python API, Part workbench primitives, Import/ImportGui STEP export fallback, plain Markdown documentation.
---
### Task 1: Asset Directory and Generator
**Files:**
- Create: `data/examples/qet_terminal_block/create_qet_terminal_slice.py`
- Create: `data/examples/qet_terminal_block/README.md`
- [ ] **Step 1: Create the generator script**
Use `apply_patch` to create `data/examples/qet_terminal_block/create_qet_terminal_slice.py`. The script must:
- Build dimensions in millimeters: width `5.2`, depth `42.0`, height `36.0`.
- Create visible geometry pieces for the green body, white top terminal area, black bottom terminal area, light green window, DIN rail slot, and two screw/contact details.
- Add two `Part::LocalCoordinateSystem` objects named `Terminal_Top` and `Terminal_Bottom`.
- Set these properties on both terminal LCS objects: `Role`, `CanWire`, `QetTemplateSlotName`, `QetTerminalLabel`, `QetTerminalType`.
- Avoid engineering binding properties: `QetProjectUuid`, `QetElementUuid`, `QetTerminalUuid`, `QetInstanceId`.
- Save `qet_terminal_slice.FCStd`.
- Export `qet_terminal_slice.step`.
- Write `qet_terminal_slice_report.json` with dimensions, output paths, and terminal property values.
- [ ] **Step 2: Create the README**
Use `apply_patch` to create `data/examples/qet_terminal_block/README.md` explaining:
- `qet_terminal_slice.FCStd` is the formal reusable FreeCAD template.
- `qet_terminal_slice.step` is geometry-only exchange output.
- The slice spacing is `5.2 mm` along X for future terminal block arrays.
- `Terminal_Top` and `Terminal_Bottom` are template terminal LCS objects.
### Task 2: Generate the Model
**Files:**
- Generate: `data/examples/qet_terminal_block/qet_terminal_slice.FCStd`
- Generate: `data/examples/qet_terminal_block/qet_terminal_slice.step`
- Generate: `data/examples/qet_terminal_block/qet_terminal_slice_report.json`
- [ ] **Step 1: Run the FreeCAD generator**
Run with the registered FreeCAD runtime:
```powershell
$runtime = Get-Content -LiteralPath 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json' -Raw | ConvertFrom-Json
$env:QET_FREECAD_RUNTIME_JSON = 'C:\Users\ng123\AppData\Local\QETDeps\runtime.json'
& $runtime.freecad_python 'D:\LightWork3D\data\examples\qet_terminal_block\create_qet_terminal_slice.py'
```
Expected: command exits `0`, and prints the generated FCStd, STEP, and report paths.
- [ ] **Step 2: Inspect output files**
Run:
```powershell
Get-ChildItem -LiteralPath 'D:\LightWork3D\data\examples\qet_terminal_block' | Select-Object Name,Length
```
Expected: `.FCStd`, `.step`, and `.json` files exist with non-zero sizes.
### Task 3: Verify Template Semantics
**Files:**
- Read: `data/examples/qet_terminal_block/qet_terminal_slice_report.json`
- Read: `data/examples/qet_terminal_block/qet_terminal_slice.FCStd`
- [ ] **Step 1: Run semantic verification**
Run a short FreeCAD Python verification command that opens `qet_terminal_slice.FCStd` and checks:
- `Terminal_Top` exists.
- `Terminal_Bottom` exists.
- Both have `Role == "Terminal"`.
- Both have `CanWire == true`.
- Both have `QetTemplateSlotName` and `QetTerminalLabel`.
- Neither has `QetProjectUuid`, `QetElementUuid`, `QetTerminalUuid`, or `QetInstanceId`.
Expected: command exits `0` and prints a success summary.
- [ ] **Step 2: Verify STEP header**
Run:
```powershell
Get-Content -LiteralPath 'D:\LightWork3D\data\examples\qet_terminal_block\qet_terminal_slice.step' -TotalCount 20
```
Expected: output contains a valid STEP header such as `ISO-10303-21`.
### Task 4: Commit the Asset
**Files:**
- Add: `docs/superpowers/plans/2026-05-26-terminal-block-model-implementation.md`
- Add: `data/examples/qet_terminal_block/create_qet_terminal_slice.py`
- Add: `data/examples/qet_terminal_block/README.md`
- Add: `data/examples/qet_terminal_block/qet_terminal_slice.FCStd`
- Add: `data/examples/qet_terminal_block/qet_terminal_slice.step`
- Add: `data/examples/qet_terminal_block/qet_terminal_slice_report.json`
- [ ] **Step 1: Check unrelated changes**
Run:
```powershell
git status --short
```
Expected: existing unrelated modified files may remain; only the plan and asset directory should be staged for this task.
- [ ] **Step 2: Stage and commit**
Run:
```powershell
git add -- docs/superpowers/plans/2026-05-26-terminal-block-model-implementation.md data/examples/qet_terminal_block
git commit -m "feat: add qet terminal block model asset"
```
Expected: a commit containing only the plan and terminal model asset files.
## Self-Review
- Spec coverage: the plan creates FCStd, STEP, generator, README, two template terminals, no engineering binding fields, and verification.
- Placeholder scan: no TBD/TODO/fill-later language is present.
- Type/property consistency: terminal object names and QET template property names match the approved design.
Loading…
Cancel
Save