/* 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 }; })();