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:
wenil
2026-05-24 18:59:50 +03:00
commit d8cb0dc06d
28 changed files with 4172 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
/* geometry.js — frontend-side preview of busbar shapes.
*
* Mirrors busbar_export.py exactly so the canvas preview is what the STEP
* will contain.
*
* panel = union of pad discs at every cell + stadium bridges between every
* pair of cells that are neighbors (distance ≤ neighborFactor ×
* min_pair_distance). Concave selections (L/U/T/...) hug their cells.
*
* wire = polyline strip of strip_width with pad discs at each cell.
*
* Welding windows (busbarHolesPath) are either:
* - cross = two perpendicular slits of length 2·hole_radius, width slit_width
* - circle = single disc of radius hole_radius
*/
const Geometry = (() => {
function neighborEdges(pts, factor) {
const n = pts.length;
if (n < 2) return [];
let minD = Infinity;
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
const dx = pts[j][0] - pts[i][0], dy = pts[j][1] - pts[i][1];
const d = Math.hypot(dx, dy);
if (d > 1e-9 && d < minD) minD = d;
}
}
if (!isFinite(minD)) return [];
const thr = minD * factor;
const out = [];
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
const dx = pts[j][0] - pts[i][0], dy = pts[j][1] - pts[i][1];
const d = Math.hypot(dx, dy);
if (d > 1e-9 && d <= thr) out.push([i, j]);
}
}
return out;
}
function busbarPath(busbar, cellsById, params) {
return (busbar.shape === "wire")
? wirePath(busbar, cellsById, params)
: panelPath(busbar, cellsById, params);
}
/** Welding windows path. Caller fills with destination-out to punch. */
function busbarHolesPath(busbar, cellsById, params) {
const path = new Path2D();
const cells = busbar.cells.map((cid) => cellsById.get(cid)).filter(Boolean);
if (params.holeShape === "circle") {
for (const c of cells) {
path.moveTo(c.x + params.holeRadius, c.y);
path.arc(c.x, c.y, params.holeRadius, 0, Math.PI * 2);
}
} else {
// cross
const halfW = Math.max(0.05, params.slitWidth / 2);
const halfL = params.holeRadius;
for (const c of cells) {
_addRectXY(path, c.x, c.y, 2 * halfL, 2 * halfW); // horizontal arm
_addRectXY(path, c.x, c.y, 2 * halfW, 2 * halfL); // vertical arm
}
}
return path;
}
function wirePath(busbar, cellsById, params) {
const path = new Path2D();
const cells = busbar.cells.map((cid) => cellsById.get(cid)).filter(Boolean);
for (const c of cells) {
path.moveTo(c.x + params.padRadius, c.y);
path.arc(c.x, c.y, params.padRadius, 0, Math.PI * 2);
}
for (let i = 0; i < cells.length - 1; i++) {
_addRect(path, cells[i], cells[i + 1], params.stripWidth);
}
return path;
}
/** Panel = disc at every cell + stadium between every neighbor pair. */
function panelPath(busbar, cellsById, params) {
const path = new Path2D();
const cells = busbar.cells.map((cid) => cellsById.get(cid)).filter(Boolean);
if (!cells.length) return path;
// Discs.
for (const c of cells) {
path.moveTo(c.x + params.padRadius, c.y);
path.arc(c.x, c.y, params.padRadius, 0, Math.PI * 2);
}
if (cells.length < 2) return path;
// Neighbor bridges — narrow connector (strip_width), not 2*pad_radius —
// this gives the dog-bone shape and a real gap to neighboring busbars.
const pts = cells.map((c) => [c.x, c.y]);
const edges = neighborEdges(pts, params.neighborFactor || 1.15);
for (const [i, j] of edges) {
_addRect(path,
{ x: pts[i][0], y: pts[i][1] },
{ x: pts[j][0], y: pts[j][1] },
params.stripWidth);
}
return path;
}
/** Rectangle from a to b of given width, as a closed Path2D sub-path.
*
* Vertex order matches Path2D.arc() winding so all sub-paths in the busbar
* body wind the SAME direction. With ctx.fill(path, "nonzero") same-winding
* subpaths union (no fill cancellation in overlap regions). If the winding
* differs from arc(), overlap regions sum to 0 and appear as holes — the
* 'tangled black gaps inside the busbar' bug.
*/
function _addRect(path, a, b, width) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const len = Math.hypot(dx, dy);
if (len < 1e-9) return;
const ux = dx / len, uy = dy / len;
const px = -uy, py = ux;
const hw = width / 2;
// Order: above-A → below-A → below-B → above-B → close.
path.moveTo(a.x + px * hw, a.y + py * hw);
path.lineTo(a.x - px * hw, a.y - py * hw);
path.lineTo(b.x - px * hw, b.y - py * hw);
path.lineTo(b.x + px * hw, b.y + py * hw);
path.closePath();
}
/** Axis-aligned rectangle centered at (cx, cy). */
function _addRectXY(path, cx, cy, w, h) {
const hw = w / 2, hh = h / 2;
path.moveTo(cx - hw, cy - hh);
path.lineTo(cx + hw, cy - hh);
path.lineTo(cx + hw, cy + hh);
path.lineTo(cx - hw, cy + hh);
path.closePath();
}
return { busbarPath, busbarHolesPath, neighborEdges, panelPath, wirePath };
})();