Files
busbar-designer/static/js/geometry.js
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

145 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 };
})();