Initial commit: Busbar Designer
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).
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user