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,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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user