d8cb0dc06d
Web tool for designing nickel/copper busbars over cylindrical-cell battery packs (21700, 18650) in hex holders. Flask + build123d backend exports STEP/DXF/SVG; vanilla JS frontend with live preview, multi-project SQLite persistence, snapshot history. Deploy scripts in deploy/ (proxmox-lxc.sh, install.sh, update.sh).
126 lines
7.4 KiB
Markdown
126 lines
7.4 KiB
Markdown
# CLAUDE.md — Busbar Designer
|
||
|
||
Notes for future sessions and any agent picking up this project.
|
||
|
||
## What this is
|
||
|
||
A small web tool to design **busbars** (nickel/copper strips) for cylindrical-cell battery packs built with hex-shaped cell holders. The user imports cell-center coordinates (originating from Addy's `Hex-Cell-Holder` OpenSCAD generator), groups cells into parallel busbars and series chains, and exports the resulting 2D geometry to **STEP / DXF / SVG**.
|
||
|
||
The hard requirement is a working **STEP export**. STEP is generated by OpenCASCADE via `build123d`'s `OCP` bindings — never write STEP by hand.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Browser (static/) ──fetch──> Flask (app.py)
|
||
├── busbar_export.py → build123d → STEP/DXF/SVG
|
||
└── storage.py → SQLite (projects/presets/snapshots)
|
||
```
|
||
|
||
- **Frontend** is plain HTML + JS (no build step). State lives in memory but is auto-synced to the server every 1.5 s; the active project ID is in the URL (`?p=42`) so refresh and other devices resume the same state.
|
||
- **Backend** is a Flask app with three groups of endpoints:
|
||
- **CAD export**: `POST /api/export/{step,dxf,svg}` — stateless; takes a busbar payload and returns bytes via build123d.
|
||
- **Persistence**: `/api/projects/*`, `/api/presets/*`, `/api/snapshots/*` — CRUD over SQLite (`storage.py`).
|
||
- **Health**: `GET /api/health`.
|
||
- The SQLite DB lives at `data/busbar.db` (override via `BUSBAR_DB` env var). In Docker it's mounted as a volume so it survives container restarts.
|
||
|
||
## Payload contract (frontend → backend)
|
||
|
||
```jsonc
|
||
{
|
||
"units": "mm",
|
||
"extrude": false, // false = flat 2D face; true = solid extrusion
|
||
"thickness": 0.2, // used only when extrude=true
|
||
"busbars": [
|
||
{
|
||
"name": "P1",
|
||
"color": "#ff8800",
|
||
"shape": "panel", // "panel" (default) | "wire"
|
||
"strip_width": 8.0, // mm — only used by shape="wire"
|
||
"pad_radius": 9.0, // mm — pad disc radius / panel inflation radius
|
||
"hole_radius": 5.0, // mm — welding window radius
|
||
"cells": [ // ordered list of cell centers in mm
|
||
{"id": 1, "x": 0.0, "y": 0.0},
|
||
{"id": 2, "x": 22.8, "y": 0.0},
|
||
{"id": 3, "x": 45.6, "y": 0.0}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**panel shape** (production-style plate — hugs ONLY selected cells, never bridges across non-selected):
|
||
`union( pad_disc(c, pad_radius) for c in cells ) ∪ union( stadium(c_i, c_j, 2*pad_radius) for (i,j) in neighbor_edges ) − holes`.
|
||
|
||
`neighbor_edges(cells, factor)` = pairs whose distance ≤ `factor × min_pair_distance` (default factor=1.15). This is what gives concave outlines (L, U, T...) instead of convex hulls. Critical: convex hull / `offset()` Minkowski-sum approaches fill in inner concavities (bridge across non-selected cells), which is **wrong** for production busbars — the user-visible bug was a fat diagonal stripe across an L-selection. The neighbor-edge stadium chain solves this.
|
||
|
||
`holes` per cell are either:
|
||
- `cross` (default): two perpendicular slits, arm length `2*hole_radius`, arm width `slit_width`.
|
||
- `circle`: disc of radius `hole_radius`.
|
||
|
||
**wire shape** (thin jumper / series link between panels):
|
||
`union( pad_disc(c) ) ∪ union( strip_segment(c[i], c[i+1], width=strip_width) ) − holes`. Cells are connected in array-order as a polyline.
|
||
|
||
## Geometry source of truth
|
||
|
||
All math comes from `hex_cell.scad` (sibling folder `Hex-Cell-Holder-master/`). Key formulas live in `static/js/importer.js`'s `generateCenters()` and **must stay in sync** with the SCAD script:
|
||
|
||
- `hex_w = cell_dia + 2*wall`
|
||
- `hex_pt = hex_w / 2 / cos(30°)`
|
||
- `opening_dia = cell_dia - 2*cell_top_overlap` (welding window default)
|
||
- Row pitch Y: `1.5 * hex_pt`
|
||
- Rect col X (even row): `hex_w * col`; (odd row): `0.5*hex_w + hex_w*col`
|
||
- Para col X: `row*0.5*hex_w + hex_w*col`
|
||
- Tria col X: `row*0.5*hex_w - hex_w*col`, `col ∈ [0..row]`
|
||
|
||
If the user reports coordinates "off by half a hex," check `rect`-vs-`para` and even/odd row offset first.
|
||
|
||
## File map
|
||
|
||
- `app.py` — Flask app. Serves `static/`, dispatches export endpoints, exposes REST CRUD for projects/presets/snapshots. Single-file; no blueprints.
|
||
- `busbar_export.py` — pure functions. Input: payload dict. Output: bytes. No Flask import here.
|
||
- `storage.py` — SQLite layer for projects, presets, snapshots. Connection-per-call inside a context manager; no Flask import.
|
||
- `static/index.html` — single page; left sidebar (import / params / busbars) + right pane (canvas + live SVG preview) + top bar (project switcher + history + exports) + history modal.
|
||
- `static/js/app.js` — top-level controller; owns in-memory state, hooks auto-save + URL routing.
|
||
- `static/js/api.js` — thin fetch-based REST client (Api.listProjects, getProject, ...).
|
||
- `static/js/importer.js` — paste-text parser + generator.
|
||
- `static/js/viewport.js` — canvas with pan/zoom; renders cells, busbars, selection.
|
||
- `static/js/groups.js` — busbar CRUD (create, rename, recolor, delete, reorder cells).
|
||
- `static/js/geometry.js` — frontend-side polygon preview.
|
||
- `static/js/exporter.js` — POSTs export payload, triggers file download.
|
||
- `scad_snippet.scad` — drop-in for `hex_cell.scad` that echoes per-cell coordinates.
|
||
|
||
## How to add a new export format
|
||
|
||
1. Add a writer in `busbar_export.py` that accepts the same payload dict and returns bytes + mimetype + extension.
|
||
2. Add a route in `app.py`: `@app.post("/api/export/<fmt>")` already dispatches by `fmt`; just register the writer in the `WRITERS` dict.
|
||
3. Add a button in `static/index.html` and hook it in `exporter.js`.
|
||
|
||
## Deployment
|
||
|
||
- **Dev**: `python app.py` → Flask debug server on `127.0.0.1:5000`.
|
||
- **Docker**: `docker compose up -d --build`. Uses Python 3.12 base (avoids the 3.13 OCP wheel risk), 2 gunicorn workers, ~600 MB image. See `Dockerfile` / `docker-compose.yml`.
|
||
- **Proxmox LXC without Docker**: clone to `/opt/busbar-designer`, create venv with `gunicorn` installed, install `deploy/busbar-designer.service` as a systemd unit. See README for the full sequence.
|
||
|
||
The app honours `HOST`, `PORT`, and `FLASK_DEBUG` env vars. Bind to `0.0.0.0` for LAN access; otherwise it stays on localhost.
|
||
|
||
No auth. Put behind a reverse proxy (Caddy/nginx/Traefik) with basic_auth or Authelia for anything beyond the LAN.
|
||
|
||
## How to swap backend for opencascade.js (WASM)
|
||
|
||
If the user ever wants zero-backend, `static/js/exporter.js` is the only file that needs to change — the payload format is identical. Drop in `opencascade.js`, build the same shapes (`Wire → Face → STEPControl_Writer`), and download via Blob.
|
||
|
||
## Tests
|
||
|
||
`tests/test_export.py` exercises `busbar_export.py` with a minimal 2-cell busbar and checks the STEP file is non-empty and contains `ISO-10303-21`. Run:
|
||
|
||
```powershell
|
||
python -m pytest tests/
|
||
```
|
||
|
||
## Known cautions
|
||
|
||
- **Python 3.13 + OCP wheels**: the official OCP wheel may not be published for the very latest CPython. If install fails, recommend Python 3.12.
|
||
- **Unit handling**: backend always treats coordinates as **mm**. Never mix units. STEP writer sets `STEPControl_Writer.WriteHeader().SetUnits("MM")` (via build123d default).
|
||
- **DXF**: `build123d.export_dxf` emits 2D entities (lines + arcs). The pad-with-hole becomes a closed boundary + an inner circle — laser CAM treats it correctly as a cut.
|
||
- **Topology**: when two pad discs overlap (cells closer than `2 * pad_radius`), use Boolean union before exporting — otherwise STEP has duplicate edges.
|