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

7.4 KiB
Raw Blame History

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)

{
  "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:

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.