Files
busbar-designer/CLAUDE.md
T
wenil d8cb0dc06d 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).
2026-05-24 18:59:50 +03:00

126 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.