feat: add wire duct and din rail model assets

dev
Zhaowenlong 4 weeks ago
parent d16dcc3fed
commit 61fa285472

@ -0,0 +1,46 @@
# QET Cabinet Assets
This directory contains reusable cabinet placement assets for QET / FreeCAD exchange examples.
## Files
- `qet_wire_duct.FCStd`: FreeCAD native wire duct asset.
- `qet_wire_duct.step`: geometry-only wire duct exchange export.
- `qet_din_rail.FCStd`: FreeCAD native DIN rail asset.
- `qet_din_rail.step`: geometry-only DIN rail exchange export.
- `qet_cabinet_assets_report.json`: generated metadata for verification.
- `create_qet_cabinet_assets.py`: FreeCAD Python generator used to recreate the assets.
## Wire Duct
The wire duct is a gray open duct for cabinet routing:
- Length: `200 mm`
- Width: `40 mm`
- Height: `40 mm`
It includes a base plate, two side walls, comb-style side slots, and mounting hole markers.
## DIN Rail
The DIN rail is a metal-gray DIN 35mm rail:
- Length: `200 mm`
- Width: `35 mm`
- Height: `7.5 mm`
It includes a hat-shaped rail profile, return lips, and elongated mounting slot markers.
## Semantics
These are placement and routing support assets. They intentionally do not contain terminal LCS objects or 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_cabinet_assets\create_qet_cabinet_assets.py'
```

@ -0,0 +1,233 @@
# Generate reusable QET cabinet assets: wire duct and DIN rail.
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:
runtime_json = os.path.join(os.environ.get("LOCALAPPDATA", ""), "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
OUT_DIR = Path(__file__).resolve().parent
REPORT_PATH = OUT_DIR / "qet_cabinet_assets_report.json"
ENGINEERING_BINDING_PROPERTIES = {
"QetProjectUuid",
"QetElementUuid",
"QetTerminalUuid",
"QetInstanceId",
}
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 _box(doc, 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_z(doc, name, radius, height, x, y, z, color):
obj = doc.addObject("Part::Feature", name)
obj.Shape = Part.makeCylinder(radius, height, App.Vector(x, y, z), App.Vector(0, 0, 1))
_style(obj, color, 0)
return obj
def _rounded_slot(doc, name, length, radius, height, x, y, z, color):
left = Part.makeCylinder(radius, height, App.Vector(x - length / 2.0 + radius, y, z), App.Vector(0, 0, 1))
right = Part.makeCylinder(radius, height, App.Vector(x + length / 2.0 - radius, y, z), App.Vector(0, 0, 1))
center = Part.makeBox(length - 2.0 * radius, radius * 2.0, height, App.Vector(x - length / 2.0 + radius, y - radius, z))
obj = doc.addObject("Part::Feature", name)
obj.Shape = left.fuse(center).fuse(right)
_style(obj, color, 0)
return obj
def _export_step(objects, path):
try:
import Import
Import.export(objects, str(path))
except Exception:
import ImportGui
ImportGui.export(objects, str(path))
def _create_wire_duct():
doc = App.newDocument("QETWireDuct")
length = 200.0
width = 40.0
height = 40.0
wall = 2.0
x0 = -length / 2.0
y0 = -width / 2.0
light_gray = (0.72, 0.74, 0.74)
dark_gray = (0.18, 0.2, 0.22)
objects = [
_box(doc, "WireDuct_BasePlate", length, width, wall, x0, y0, 0.0, light_gray),
_box(doc, "WireDuct_LeftWall", length, wall, height, x0, y0, 0.0, light_gray),
_box(doc, "WireDuct_RightWall", length, wall, height, x0, width / 2.0 - wall, 0.0, light_gray),
]
slot_count = 18
slot_pitch = length / slot_count
finger_width = slot_pitch * 0.45
for index in range(slot_count):
center_x = x0 + slot_pitch * (index + 0.5)
objects.append(
_box(
doc,
"WireDuct_LeftCombSlot_{0:02d}".format(index + 1),
finger_width,
wall + 0.2,
height - 8.0,
center_x - finger_width / 2.0,
y0 - 0.1,
8.0,
dark_gray,
)
)
objects.append(
_box(
doc,
"WireDuct_RightCombSlot_{0:02d}".format(index + 1),
finger_width,
wall + 0.2,
height - 8.0,
center_x - finger_width / 2.0,
width / 2.0 - wall - 0.1,
8.0,
dark_gray,
)
)
for center_x in (-60.0, 0.0, 60.0):
objects.append(_cylinder_z(doc, "WireDuct_MountHole_{0:g}".format(center_x), 2.2, wall + 0.2, center_x, 0.0, 0.0, dark_gray))
doc.recompute()
fcstd = OUT_DIR / "qet_wire_duct.FCStd"
step = OUT_DIR / "qet_wire_duct.step"
doc.saveAs(str(fcstd))
_export_step(objects, step)
return {
"name": "wire_duct",
"fcstd": str(fcstd),
"step": str(step),
"dimensions_mm": {"length": length, "width": width, "height": height},
"objects": [obj.Name for obj in objects],
}
def _create_din_rail():
doc = App.newDocument("QETDINRail")
length = 200.0
width = 35.0
height = 7.5
x0 = -length / 2.0
y0 = -width / 2.0
metal = (0.55, 0.57, 0.58)
dark = (0.16, 0.17, 0.18)
objects = [
_box(doc, "DINRail_CenterTop", length, 15.0, 2.0, x0, -7.5, height - 2.0, metal),
_box(doc, "DINRail_LeftWeb", length, 2.0, height, x0, -9.5, 0.0, metal),
_box(doc, "DINRail_RightWeb", length, 2.0, height, x0, 7.5, 0.0, metal),
_box(doc, "DINRail_LeftFlange", length, 8.0, 1.6, x0, y0, 0.0, metal),
_box(doc, "DINRail_RightFlange", length, 8.0, 1.6, x0, width / 2.0 - 8.0, 0.0, metal),
_box(doc, "DINRail_LeftReturnLip", length, 2.2, 2.0, x0, y0, 1.4, metal),
_box(doc, "DINRail_RightReturnLip", length, 2.2, 2.0, x0, width / 2.0 - 2.2, 1.4, metal),
]
for center_x in (-60.0, 0.0, 60.0):
objects.append(_rounded_slot(doc, "DINRail_MountSlot_{0:g}".format(center_x), 12.0, 2.2, 0.35, center_x, 0.0, height - 1.95, dark))
doc.recompute()
fcstd = OUT_DIR / "qet_din_rail.FCStd"
step = OUT_DIR / "qet_din_rail.step"
doc.saveAs(str(fcstd))
_export_step(objects, step)
return {
"name": "din_rail",
"fcstd": str(fcstd),
"step": str(step),
"dimensions_mm": {"length": length, "width": width, "height": height},
"objects": [obj.Name for obj in objects],
}
def _binding_properties_present(doc):
found = {}
for obj in doc.Objects:
props = [prop for prop in sorted(ENGINEERING_BINDING_PROPERTIES) if prop in obj.PropertiesList]
if props:
found[obj.Name] = props
return found
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
assets = [_create_wire_duct(), _create_din_rail()]
report = {"assets": assets}
REPORT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
for asset in assets:
print("Generated {0} FCStd: {1}".format(asset["name"], asset["fcstd"]))
print("Generated {0} STEP: {1}".format(asset["name"], asset["step"]))
print("Generated report: {0}".format(REPORT_PATH))
if __name__ == "__main__":
main()

@ -0,0 +1,80 @@
{
"assets": [
{
"name": "wire_duct",
"fcstd": "D:\\LightWork3D\\data\\examples\\qet_cabinet_assets\\qet_wire_duct.FCStd",
"step": "D:\\LightWork3D\\data\\examples\\qet_cabinet_assets\\qet_wire_duct.step",
"dimensions_mm": {
"length": 200.0,
"width": 40.0,
"height": 40.0
},
"objects": [
"WireDuct_BasePlate",
"WireDuct_LeftWall",
"WireDuct_RightWall",
"WireDuct_LeftCombSlot_01",
"WireDuct_RightCombSlot_01",
"WireDuct_LeftCombSlot_02",
"WireDuct_RightCombSlot_02",
"WireDuct_LeftCombSlot_03",
"WireDuct_RightCombSlot_03",
"WireDuct_LeftCombSlot_04",
"WireDuct_RightCombSlot_04",
"WireDuct_LeftCombSlot_05",
"WireDuct_RightCombSlot_05",
"WireDuct_LeftCombSlot_06",
"WireDuct_RightCombSlot_06",
"WireDuct_LeftCombSlot_07",
"WireDuct_RightCombSlot_07",
"WireDuct_LeftCombSlot_08",
"WireDuct_RightCombSlot_08",
"WireDuct_LeftCombSlot_09",
"WireDuct_RightCombSlot_09",
"WireDuct_LeftCombSlot_10",
"WireDuct_RightCombSlot_10",
"WireDuct_LeftCombSlot_11",
"WireDuct_RightCombSlot_11",
"WireDuct_LeftCombSlot_12",
"WireDuct_RightCombSlot_12",
"WireDuct_LeftCombSlot_13",
"WireDuct_RightCombSlot_13",
"WireDuct_LeftCombSlot_14",
"WireDuct_RightCombSlot_14",
"WireDuct_LeftCombSlot_15",
"WireDuct_RightCombSlot_15",
"WireDuct_LeftCombSlot_16",
"WireDuct_RightCombSlot_16",
"WireDuct_LeftCombSlot_17",
"WireDuct_RightCombSlot_17",
"WireDuct_LeftCombSlot_18",
"WireDuct_RightCombSlot_18",
"WireDuct_MountHole__60",
"WireDuct_MountHole_0",
"WireDuct_MountHole_60"
]
},
{
"name": "din_rail",
"fcstd": "D:\\LightWork3D\\data\\examples\\qet_cabinet_assets\\qet_din_rail.FCStd",
"step": "D:\\LightWork3D\\data\\examples\\qet_cabinet_assets\\qet_din_rail.step",
"dimensions_mm": {
"length": 200.0,
"width": 35.0,
"height": 7.5
},
"objects": [
"DINRail_CenterTop",
"DINRail_LeftWeb",
"DINRail_RightWeb",
"DINRail_LeftFlange",
"DINRail_RightFlange",
"DINRail_LeftReturnLip",
"DINRail_RightReturnLip",
"DINRail_MountSlot__60",
"DINRail_MountSlot_0",
"DINRail_MountSlot_60"
]
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,100 @@
# Wire Duct and DIN Rail Assets 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 reusable FreeCAD and STEP assets for one wire duct and one DIN 35mm rail.
**Architecture:** Add one focused FreeCAD Python generator under `data/examples/qet_cabinet_assets/`. The script bootstraps the registered Windows FreeCAD runtime, creates separate FreeCAD documents for the wire duct and DIN rail, saves `.FCStd`, exports `.step`, and writes a JSON report for verification.
**Tech Stack:** FreeCAD Python API, Part workbench primitives, registered QETDeps FreeCAD Python runtime, Markdown documentation.
---
### Task 1: Generator and README
**Files:**
- Create: `data/examples/qet_cabinet_assets/create_qet_cabinet_assets.py`
- Create: `data/examples/qet_cabinet_assets/README.md`
- [ ] **Step 1: Create the generator**
Create `create_qet_cabinet_assets.py`. It must:
- Bootstrap Windows FreeCAD DLL paths from `QET_FREECAD_RUNTIME_JSON` or `%LOCALAPPDATA%\QETDeps\runtime.json`.
- Generate `qet_wire_duct.FCStd` and `qet_wire_duct.step`.
- Generate `qet_din_rail.FCStd` and `qet_din_rail.step`.
- Write `qet_cabinet_assets_report.json`.
- Use dimensions from the approved spec.
- Avoid creating terminal LCS objects or engineering binding properties.
- [ ] **Step 2: Create the README**
Create `README.md` describing the two assets, dimensions, file roles, regeneration command, and the fact that neither model contains terminal semantics.
### Task 2: Generate and Verify
**Files:**
- Generate: `data/examples/qet_cabinet_assets/qet_wire_duct.FCStd`
- Generate: `data/examples/qet_cabinet_assets/qet_wire_duct.step`
- Generate: `data/examples/qet_cabinet_assets/qet_din_rail.FCStd`
- Generate: `data/examples/qet_cabinet_assets/qet_din_rail.step`
- Generate: `data/examples/qet_cabinet_assets/qet_cabinet_assets_report.json`
- [ ] **Step 1: Run generator**
Run:
```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_cabinet_assets\create_qet_cabinet_assets.py'
```
Expected: command exits `0` and prints all generated output paths.
- [ ] **Step 2: Verify files**
Run:
```powershell
Get-ChildItem -LiteralPath 'D:\LightWork3D\data\examples\qet_cabinet_assets' | Select-Object Name,Length
```
Expected: four model files and one report exist with non-zero sizes.
- [ ] **Step 3: Verify FCStd documents and STEP headers**
Open both FCStd documents with the registered FreeCAD Python runtime and assert that objects exist, no object has `Role="Terminal"`, and no engineering binding properties exist. Read the first line of both STEP files and assert it is `ISO-10303-21;`.
### Task 3: Commit
**Files:**
- Add: `docs/superpowers/plans/2026-05-26-wire-duct-din-rail-assets-implementation.md`
- Add: `data/examples/qet_cabinet_assets/`
- [ ] **Step 1: Stage intended files only**
Run:
```powershell
git add -- docs/superpowers/plans/2026-05-26-wire-duct-din-rail-assets-implementation.md data/examples/qet_cabinet_assets
git diff --cached --name-only
```
Expected: only the plan and `qet_cabinet_assets` files are staged.
- [ ] **Step 2: Commit**
Run:
```powershell
git commit -m "feat: add wire duct and din rail model assets"
```
Expected: a commit containing only the generated assets, generator, report, README, and plan.
## Self-Review
- Spec coverage: implements both FCStd and STEP outputs for the line duct and DIN rail, report, README, and validation.
- Placeholder scan: no TBD/TODO/fill-later language is present.
- Type consistency: output file names match the approved spec.
Loading…
Cancel
Save