feat: add qet terminal block model asset
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))
|
||||
Binary file not shown.
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…
Reference in New Issue