d8cb0dc06d
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).
145 lines
5.0 KiB
JavaScript
145 lines
5.0 KiB
JavaScript
/* 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 };
|
||
})();
|