# 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 (busbars page) ├── holder.py → openscad subprocess → STL (holder page) └── storage.py → SQLite (projects/presets/snapshots) ``` Two pages: - `/` → busbar designer (cell-import, panel/wire shapes, export STEP) - `/holder` → hex-holder designer (parameter form, openscad render, STL download, "Design busbars →" hand-off) - **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/holder. Single-file; no blueprints. - `busbar_export.py` — pure functions. Input: payload dict. Output: bytes. No Flask import here. - `holder.py` — `subprocess.run(["openscad", "-D ...", "hex_cell.scad"])` wrapper that returns STL bytes; also has `compute_cells()` Python-side (mirrors the SCAD formulas) so the busbar hand-off doesn't need a second openscad call. - `scad/hex_cell.scad` — verbatim copy of Addy777/Hex-Cell-Holder script. Source of truth for hex holder geometry. Update via git (no submodule for now). - `storage.py` — SQLite layer for projects, presets, snapshots. Connection-per-call inside a context manager; no Flask import. - `static/index.html` — busbar designer page; left sidebar (import / params / busbars) + right pane (canvas + live SVG preview) + top bar (project switcher + history + exports) + history modal. - `static/holder.html` — hex-holder designer page; parameter form (left) + Three.js viewer (right). - `static/js/holder-app.js`, `holder-viewer.js`, `holder.css` — UI for `/holder`. holder-viewer is an ES module (Three.js imported via importmap from jsdelivr). - `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/")` 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.