feat: add panel assembly model asset

dev
Zhaowenlong 4 weeks ago
parent 954b4f84ab
commit 3f6b07eb37

@ -0,0 +1,43 @@
# QET Panel Assembly Model
This directory contains a reusable electrical panel assembly model based on the referenced video.
## Files
- `qet_panel_assembly.FCStd`: FreeCAD native panel assembly asset.
- `qet_panel_assembly.step`: geometry-only exchange export.
- `qet_panel_assembly_report.json`: generated metadata for verification.
- `create_qet_panel_assembly.py`: FreeCAD Python generator used to recreate the asset.
## Geometry
The model is a medium-detail approximation of the full assembly:
- Pale gray cabinet / door body.
- Thick side edge and recessed door panel.
- Two circular hinge / screw markers.
- Dark right-side mounting plate.
- Two vertical white perforated connector banks.
- Small lower accessory connector modules.
- Yellow wire-frame style guide geometry matching the highlighted wiring envelope in the reference.
Approximate dimensions:
- Overall height: `180 mm`
- Cabinet width: `110 mm`
- Cabinet depth: `55 mm`
- Right connector area height: about `120 mm`
## Semantics
This is a visual and placement asset. It intentionally does 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_panel_assembly\create_qet_panel_assembly.py'
```

@ -0,0 +1,246 @@
# Generate a reusable QET electrical panel assembly asset.
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
FCSTD_PATH = OUT_DIR / "qet_panel_assembly.FCStd"
STEP_PATH = OUT_DIR / "qet_panel_assembly.step"
REPORT_PATH = OUT_DIR / "qet_panel_assembly_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_y(doc, 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 _wire_segment(doc, name, start, end, thickness, color):
sx, sy, sz = start
ex, ey, ez = end
dx = abs(ex - sx) or thickness
dy = abs(ey - sy) or thickness
dz = abs(ez - sz) or thickness
x = min(sx, ex) - (thickness / 2.0 if ex == sx else 0.0)
y = min(sy, ey) - (thickness / 2.0 if ey == sy else 0.0)
z = min(sz, ez) - (thickness / 2.0 if ez == sz else 0.0)
return _box(doc, name, dx, dy, dz, x, y, z, color, 0)
def _wire_rect(doc, prefix, x0, y0, z0, width, height, depth_offset=0.0):
color = (1.0, 0.86, 0.05)
y = y0 + depth_offset
thickness = 0.55
return [
_wire_segment(doc, prefix + "_Left", (x0, y, z0), (x0, y, z0 + height), thickness, color),
_wire_segment(doc, prefix + "_Right", (x0 + width, y, z0), (x0 + width, y, z0 + height), thickness, color),
_wire_segment(doc, prefix + "_Top", (x0, y, z0 + height), (x0 + width, y, z0 + height), thickness, color),
_wire_segment(doc, prefix + "_Bottom", (x0, y, z0), (x0 + width, y, z0), thickness, color),
]
def _export_step(objects):
try:
import Import
Import.export(objects, str(STEP_PATH))
except Exception:
import ImportGui
ImportGui.export(objects, str(STEP_PATH))
def _create_connector_bank(doc, prefix, x, y, z, rows, cols, plate_height, plate_width):
white = (0.93, 0.9, 0.95)
dark = (0.04, 0.04, 0.05)
metal = (0.58, 0.58, 0.56)
objects = [
_box(doc, prefix + "_Body", plate_width, 4.0, plate_height, x, y, z, white, 0),
_box(doc, prefix + "_SideRailLeft", 1.2, 4.4, plate_height, x - 1.2, y - 0.2, z, metal, 0),
_box(doc, prefix + "_SideRailRight", 1.2, 4.4, plate_height, x + plate_width, y - 0.2, z, metal, 0),
]
x_pitch = plate_width / (cols + 1)
z_pitch = plate_height / (rows + 1)
for row in range(rows):
for col in range(cols):
px = x + x_pitch * (col + 1)
pz = z + z_pitch * (row + 1)
objects.append(
_cylinder_y(
doc,
"{0}_Hole_R{1:02d}_C{2:02d}".format(prefix, row + 1, col + 1),
1.0,
0.7,
px,
y - 0.15,
pz,
dark,
)
)
if col == 0:
objects.append(
_cylinder_y(
doc,
"{0}_Screw_R{1:02d}".format(prefix, row + 1),
0.55,
0.8,
x + plate_width + 2.6,
y - 0.15,
pz,
metal,
)
)
return objects
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
doc = App.newDocument("QETPanelAssembly")
objects = []
light = (0.82, 0.84, 0.88)
edge = (0.46, 0.43, 0.5)
dark_panel = (0.22, 0.2, 0.26)
white = (0.95, 0.94, 0.96)
black = (0.02, 0.02, 0.02)
# Cabinet / door, roughly matching the large pale box in the reference video.
objects.extend(
[
_box(doc, "Panel_BackBox", 110.0, 55.0, 180.0, -80.0, -25.0, 0.0, light, 0),
_box(doc, "Panel_LeftDoorFace", 6.0, 60.0, 178.0, -88.0, -28.0, 1.0, edge, 0),
_box(doc, "Panel_InnerRecess", 72.0, 1.4, 132.0, -68.0, -29.0, 24.0, (0.9, 0.92, 0.96), 18),
_box(doc, "Panel_RecessLeftLine", 1.2, 1.8, 132.0, -68.0, -30.0, 24.0, edge, 0),
_box(doc, "Panel_RecessRightLine", 1.2, 1.8, 132.0, 2.8, -30.0, 24.0, edge, 0),
_box(doc, "Panel_RecessTopLine", 72.0, 1.8, 1.2, -68.0, -30.0, 155.0, edge, 0),
_box(doc, "Panel_RecessBottomLine", 72.0, 1.8, 1.2, -68.0, -30.0, 24.0, edge, 0),
_cylinder_y(doc, "Panel_HingeTop", 2.3, 1.0, -85.0, -31.0, 134.0, (0.18, 0.18, 0.2)),
_cylinder_y(doc, "Panel_HingeBottom", 2.3, 1.0, -85.0, -31.0, 44.0, (0.18, 0.18, 0.2)),
_box(doc, "Panel_RightMountPlate", 45.0, 6.0, 168.0, 30.0, -31.0, 6.0, dark_panel, 0),
]
)
# Connector banks and small accessory blocks on the right side.
objects.extend(_create_connector_bank(doc, "ConnectorBank_Left", 35.0, -36.0, 44.0, 10, 2, 110.0, 18.0))
objects.extend(_create_connector_bank(doc, "ConnectorBank_Right", 60.0, -37.0, 50.0, 12, 3, 102.0, 20.0))
objects.extend(
[
_box(doc, "ConnectorBank_LeftTopCap", 18.0, 4.2, 12.0, 35.0, -36.2, 154.0, white, 0),
_box(doc, "ConnectorBank_RightTopCap", 20.0, 4.2, 11.0, 60.0, -37.2, 152.0, white, 0),
_box(doc, "AccessoryConnector_LowerLeft", 16.0, 4.0, 34.0, 26.0, -37.0, 18.0, white, 0),
_box(doc, "AccessoryConnector_LowerRight", 14.0, 4.0, 28.0, 70.0, -38.0, 22.0, white, 0),
_cylinder_y(doc, "AccessoryConnector_LowerLeftScrew1", 0.8, 0.7, 34.0, -38.1, 28.0, black),
_cylinder_y(doc, "AccessoryConnector_LowerLeftScrew2", 0.8, 0.7, 34.0, -38.1, 42.0, black),
_cylinder_y(doc, "AccessoryConnector_LowerRightScrew1", 0.8, 0.7, 77.0, -39.1, 30.0, black),
_cylinder_y(doc, "AccessoryConnector_LowerRightScrew2", 0.8, 0.7, 77.0, -39.1, 44.0, black),
]
)
# Yellow annotation-like wire frames from the source video.
objects.extend(_wire_rect(doc, "WireFrame_LeftBank", 32.0, -41.0, 34.0, 27.0, 130.0))
objects.extend(_wire_rect(doc, "WireFrame_RightBank", 56.0, -42.0, 38.0, 33.0, 126.0, -1.2))
objects.extend(
[
_wire_segment(doc, "WireFrame_TopBridge", (45.0, -42.0, 158.0), (88.0, -42.0, 158.0), 0.55, (1.0, 0.86, 0.05)),
_wire_segment(doc, "WireFrame_BottomBridge", (42.0, -42.0, 34.0), (88.0, -42.0, 34.0), 0.55, (1.0, 0.86, 0.05)),
]
)
doc.recompute()
doc.saveAs(str(FCSTD_PATH))
_export_step(objects)
report = {
"dimensions_mm": {
"overall_height": 180.0,
"cabinet_width": 110.0,
"cabinet_depth": 55.0,
"connector_area_height": 120.0,
},
"outputs": {"fcstd": str(FCSTD_PATH), "step": str(STEP_PATH)},
"objects": [obj.Name for obj in objects],
"engineering_binding_properties_present": {
obj.Name: [prop for prop in sorted(ENGINEERING_BINDING_PROPERTIES) if prop in obj.PropertiesList]
for obj in objects
if any(prop in obj.PropertiesList for prop in ENGINEERING_BINDING_PROPERTIES)
},
"terminal_role_objects": [obj.Name for obj in objects if getattr(obj, "Role", "") == "Terminal"],
}
REPORT_PATH.write_text(json.dumps(report, indent=2), encoding="utf-8")
print("Generated FCStd: {0}".format(FCSTD_PATH))
print("Generated STEP: {0}".format(STEP_PATH))
print("Generated report: {0}".format(REPORT_PATH))
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

@ -0,0 +1,128 @@
{
"dimensions_mm": {
"overall_height": 180.0,
"cabinet_width": 110.0,
"cabinet_depth": 55.0,
"connector_area_height": 120.0
},
"outputs": {
"fcstd": "D:\\LightWork3D\\data\\examples\\qet_panel_assembly\\qet_panel_assembly.FCStd",
"step": "D:\\LightWork3D\\data\\examples\\qet_panel_assembly\\qet_panel_assembly.step"
},
"objects": [
"Panel_BackBox",
"Panel_LeftDoorFace",
"Panel_InnerRecess",
"Panel_RecessLeftLine",
"Panel_RecessRightLine",
"Panel_RecessTopLine",
"Panel_RecessBottomLine",
"Panel_HingeTop",
"Panel_HingeBottom",
"Panel_RightMountPlate",
"ConnectorBank_Left_Body",
"ConnectorBank_Left_SideRailLeft",
"ConnectorBank_Left_SideRailRight",
"ConnectorBank_Left_Hole_R01_C01",
"ConnectorBank_Left_Screw_R01",
"ConnectorBank_Left_Hole_R01_C02",
"ConnectorBank_Left_Hole_R02_C01",
"ConnectorBank_Left_Screw_R02",
"ConnectorBank_Left_Hole_R02_C02",
"ConnectorBank_Left_Hole_R03_C01",
"ConnectorBank_Left_Screw_R03",
"ConnectorBank_Left_Hole_R03_C02",
"ConnectorBank_Left_Hole_R04_C01",
"ConnectorBank_Left_Screw_R04",
"ConnectorBank_Left_Hole_R04_C02",
"ConnectorBank_Left_Hole_R05_C01",
"ConnectorBank_Left_Screw_R05",
"ConnectorBank_Left_Hole_R05_C02",
"ConnectorBank_Left_Hole_R06_C01",
"ConnectorBank_Left_Screw_R06",
"ConnectorBank_Left_Hole_R06_C02",
"ConnectorBank_Left_Hole_R07_C01",
"ConnectorBank_Left_Screw_R07",
"ConnectorBank_Left_Hole_R07_C02",
"ConnectorBank_Left_Hole_R08_C01",
"ConnectorBank_Left_Screw_R08",
"ConnectorBank_Left_Hole_R08_C02",
"ConnectorBank_Left_Hole_R09_C01",
"ConnectorBank_Left_Screw_R09",
"ConnectorBank_Left_Hole_R09_C02",
"ConnectorBank_Left_Hole_R10_C01",
"ConnectorBank_Left_Screw_R10",
"ConnectorBank_Left_Hole_R10_C02",
"ConnectorBank_Right_Body",
"ConnectorBank_Right_SideRailLeft",
"ConnectorBank_Right_SideRailRight",
"ConnectorBank_Right_Hole_R01_C01",
"ConnectorBank_Right_Screw_R01",
"ConnectorBank_Right_Hole_R01_C02",
"ConnectorBank_Right_Hole_R01_C03",
"ConnectorBank_Right_Hole_R02_C01",
"ConnectorBank_Right_Screw_R02",
"ConnectorBank_Right_Hole_R02_C02",
"ConnectorBank_Right_Hole_R02_C03",
"ConnectorBank_Right_Hole_R03_C01",
"ConnectorBank_Right_Screw_R03",
"ConnectorBank_Right_Hole_R03_C02",
"ConnectorBank_Right_Hole_R03_C03",
"ConnectorBank_Right_Hole_R04_C01",
"ConnectorBank_Right_Screw_R04",
"ConnectorBank_Right_Hole_R04_C02",
"ConnectorBank_Right_Hole_R04_C03",
"ConnectorBank_Right_Hole_R05_C01",
"ConnectorBank_Right_Screw_R05",
"ConnectorBank_Right_Hole_R05_C02",
"ConnectorBank_Right_Hole_R05_C03",
"ConnectorBank_Right_Hole_R06_C01",
"ConnectorBank_Right_Screw_R06",
"ConnectorBank_Right_Hole_R06_C02",
"ConnectorBank_Right_Hole_R06_C03",
"ConnectorBank_Right_Hole_R07_C01",
"ConnectorBank_Right_Screw_R07",
"ConnectorBank_Right_Hole_R07_C02",
"ConnectorBank_Right_Hole_R07_C03",
"ConnectorBank_Right_Hole_R08_C01",
"ConnectorBank_Right_Screw_R08",
"ConnectorBank_Right_Hole_R08_C02",
"ConnectorBank_Right_Hole_R08_C03",
"ConnectorBank_Right_Hole_R09_C01",
"ConnectorBank_Right_Screw_R09",
"ConnectorBank_Right_Hole_R09_C02",
"ConnectorBank_Right_Hole_R09_C03",
"ConnectorBank_Right_Hole_R10_C01",
"ConnectorBank_Right_Screw_R10",
"ConnectorBank_Right_Hole_R10_C02",
"ConnectorBank_Right_Hole_R10_C03",
"ConnectorBank_Right_Hole_R11_C01",
"ConnectorBank_Right_Screw_R11",
"ConnectorBank_Right_Hole_R11_C02",
"ConnectorBank_Right_Hole_R11_C03",
"ConnectorBank_Right_Hole_R12_C01",
"ConnectorBank_Right_Screw_R12",
"ConnectorBank_Right_Hole_R12_C02",
"ConnectorBank_Right_Hole_R12_C03",
"ConnectorBank_LeftTopCap",
"ConnectorBank_RightTopCap",
"AccessoryConnector_LowerLeft",
"AccessoryConnector_LowerRight",
"AccessoryConnector_LowerLeftScrew1",
"AccessoryConnector_LowerLeftScrew2",
"AccessoryConnector_LowerRightScrew1",
"AccessoryConnector_LowerRightScrew2",
"WireFrame_LeftBank_Left",
"WireFrame_LeftBank_Right",
"WireFrame_LeftBank_Top",
"WireFrame_LeftBank_Bottom",
"WireFrame_RightBank_Left",
"WireFrame_RightBank_Right",
"WireFrame_RightBank_Top",
"WireFrame_RightBank_Bottom",
"WireFrame_TopBridge",
"WireFrame_BottomBridge"
],
"engineering_binding_properties_present": {},
"terminal_role_objects": []
}

@ -0,0 +1,97 @@
# Panel Assembly Model Asset 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 electrical panel assembly asset as `.FCStd` and `.step`.
**Architecture:** Add one FreeCAD Python generator in `data/examples/qet_panel_assembly/`. The script bootstraps the registered Windows FreeCAD runtime, creates the panel assembly geometry, saves the FCStd, exports STEP, and writes a report used by 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_panel_assembly/create_qet_panel_assembly.py`
- Create: `data/examples/qet_panel_assembly/README.md`
- [ ] **Step 1: Create generator**
Create `create_qet_panel_assembly.py`. It must:
- Bootstrap Windows FreeCAD DLL paths from `QET_FREECAD_RUNTIME_JSON` or `%LOCALAPPDATA%\QETDeps\runtime.json`.
- Generate cabinet/door geometry, dark mounting plate, two white perforated connector banks, small white connector modules, and yellow wire frame geometry.
- Save `qet_panel_assembly.FCStd`.
- Export `qet_panel_assembly.step`.
- Write `qet_panel_assembly_report.json`.
- Avoid terminal LCS objects and engineering binding properties.
- [ ] **Step 2: Create README**
Create `README.md` describing dimensions, visual parts, output file roles, regeneration command, and the absence of terminal semantics.
### Task 2: Generate and Verify
**Files:**
- Generate: `data/examples/qet_panel_assembly/qet_panel_assembly.FCStd`
- Generate: `data/examples/qet_panel_assembly/qet_panel_assembly.step`
- Generate: `data/examples/qet_panel_assembly/qet_panel_assembly_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_panel_assembly\create_qet_panel_assembly.py'
```
Expected: command exits `0` and prints generated output paths.
- [ ] **Step 2: Verify FCStd and STEP**
Open the FCStd with the registered FreeCAD Python runtime and assert:
- The document has objects.
- At least one object name starts with `Panel_`.
- At least one object name starts with `ConnectorBank_`.
- At least one object name starts with `WireFrame_`.
- No object has `Role="Terminal"`.
- No object has `QetProjectUuid`, `QetElementUuid`, `QetTerminalUuid`, or `QetInstanceId`.
Read the first line of the STEP file and assert it is `ISO-10303-21;`.
### Task 3: Commit
**Files:**
- Add: `docs/superpowers/plans/2026-05-27-panel-assembly-model-implementation.md`
- Add: `data/examples/qet_panel_assembly/`
- [ ] **Step 1: Stage intended files only**
Run:
```powershell
git add -- docs/superpowers/plans/2026-05-27-panel-assembly-model-implementation.md data/examples/qet_panel_assembly
git diff --cached --name-only
```
Expected: only the plan and `qet_panel_assembly` files are staged.
- [ ] **Step 2: Commit**
Run:
```powershell
git commit -m "feat: add panel assembly model asset"
```
Expected: a commit containing only the generated panel assembly asset files and plan.
## Self-Review
- Spec coverage: implements FCStd, STEP, generator, README, report, and semantic verification.
- Placeholder scan: no TBD/TODO/fill-later language is present.
- Type consistency: output file names match the approved spec.
Loading…
Cancel
Save