/*! * Tectum * Copyright © 2026 Machiel Heinen. * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * Zie LICENTIE.md. */ // Plain script, geen ES-module: imports breken bij file:// (CORS). function calculateSegmentPositions(spanWidth, minSp, maxSp) { if (spanWidth <= 0) return []; let numBays = Math.ceil(spanWidth / maxSp); let spacing = spanWidth / numBays; if (spacing < minSp && numBays > 1) { numBays = Math.floor(spanWidth / minSp); spacing = spanWidth / numBays; } const positions = []; for (let i = 1; i < numBays; i++) { positions.push(i * spacing); } return positions; } function polygonArea(points) { let area = 0; for (let i = 0; i < points.length; i++) { const p1 = points[i]; const p2 = points[(i + 1) % points.length]; area += p1.x * p2.y - p2.x * p1.y; } return Math.abs(area) / 2; } function clipPolygonYLE(points, yLimit) { const result = []; for (let i = 0; i < points.length; i++) { const cur = points[i]; const prev = points[(i - 1 + points.length) % points.length]; const curIn = cur.y <= yLimit + 1e-12; const prevIn = prev.y <= yLimit + 1e-12; if (curIn) { if (!prevIn) { const t = (yLimit - prev.y) / (cur.y - prev.y); result.push({ x: prev.x + t * (cur.x - prev.x), y: yLimit }); } result.push(cur); } else if (prevIn) { const t = (yLimit - prev.y) / (cur.y - prev.y); result.push({ x: prev.x + t * (cur.x - prev.x), y: yLimit }); } } return result; } function clipPolygonYGE(points, yLimit) { const result = []; for (let i = 0; i < points.length; i++) { const cur = points[i]; const prev = points[(i - 1 + points.length) % points.length]; const curIn = cur.y >= yLimit - 1e-12; const prevIn = prev.y >= yLimit - 1e-12; if (curIn) { if (!prevIn) { const t = (yLimit - prev.y) / (cur.y - prev.y); result.push({ x: prev.x + t * (cur.x - prev.x), y: yLimit }); } result.push(cur); } else if (prevIn) { const t = (yLimit - prev.y) / (cur.y - prev.y); result.push({ x: prev.x + t * (cur.x - prev.x), y: yLimit }); } } return result; } function shapeAreaWithHoles(shape) { let area = polygonArea(shape.getPoints()); (shape.holes || []).forEach(hole => { area -= polygonArea(hole.getPoints()); }); return area; } function dist2D(a, b) { return Math.hypot(a.x - b.x, a.y - b.y); } // Zaagsnede-breedte wordt verwaarloosd. function planStockCutting(pieceLengths, stockLengths) { const stocks = stockLengths.slice().sort((a, b) => a - b); const maxStock = stocks[stocks.length - 1]; const parts = []; pieceLengths.forEach(len => { let rest = Math.round(len); while (rest > maxStock) { parts.push(maxStock); rest -= maxStock; } if (rest > 0) parts.push(rest); }); parts.sort((a, b) => b - a); const bars = []; parts.forEach(p => { let best = null, bestRest = Infinity; bars.forEach(bar => { const r = maxStock - bar.used - p; if (r >= 0 && r < bestRest) { best = bar; bestRest = r; } }); if (!best) { best = { used: 0, cuts: [] }; bars.push(best); } best.cuts.push(p); best.used += p; }); bars.forEach(bar => { bar.stock = stocks.find(s => s >= bar.used); bar.waste = bar.stock - bar.used; }); return bars; } function summarizeStockPlan(bars) { const byStock = new Map(); bars.forEach(b => byStock.set(b.stock, (byStock.get(b.stock) || 0) + 1)); return { perStock: Array.from(byStock.entries()).sort((a, b) => a[0] - b[0]).map(([stock, count]) => ({ stock, count })), stockMM: bars.reduce((a, b) => a + b.stock, 0), wasteMM: bars.reduce((a, b) => a + b.waste, 0) }; } // Een uitsparing over de rand wordt een inkeping, geen afgekapt gat (dat liet een haarfijn // reepje materiaal met randlijnen door de opening lopen). Schuin (y1r) alleen bovenin. function columnMinusRects(x0, x1, yBottom, topYAt, rects, opts) { const eps = 1e-9; const yCap = (opts && opts.yCap !== undefined) ? opts.yCap : Infinity; const top = (x) => Math.min(yCap, topYAt(x)); const uniq = (arr) => arr.sort((a, b) => a - b).filter((v, i, a) => i === 0 || v - a[i - 1] > eps); const rs = rects .filter(r => Math.min(x1, r.x1) - Math.max(x0, r.x0) > eps) .map(r => { const y1r = r.y1r !== undefined ? r.y1r : r.y1, s = (y1r - r.y1) / (r.x1 - r.x0); return { x0: Math.max(x0, r.x0), x1: Math.min(x1, r.x1), y0: r.y0, topAt: (x) => r.y1 + (x - r.x0) * s }; }); const inRect = (x, y) => rs.some(r => x > r.x0 && x < r.x1 && y > r.y0 && y < r.topAt(x)); const raw = [() => yBottom, top]; rs.forEach(r => raw.push(() => r.y0, r.topAt)); const clamp = (f) => (x) => Math.min(top(x), Math.max(yBottom, f(x))); const lines = raw.map(clamp); let xs = uniq([x0, x1, ...((opts && opts.extraXs) || []).filter(x => x > x0 + eps && x < x1 - eps), ...rs.flatMap(r => [r.x0, r.x1])]); const cuts = []; for (let i = 0; i < xs.length - 1; i++) { const a = xs[i], b = xs[i + 1]; for (let p = 0; p < raw.length; p++) { for (let q = p + 1; q < raw.length; q++) { const da = raw[p](a) - raw[q](a), db = raw[p](b) - raw[q](b); if ((da > eps && db < -eps) || (da < -eps && db > eps)) cuts.push(a + (b - a) * da / (da - db)); } } } xs = uniq(xs.concat(cuts)); const cols = []; for (let i = 0; i < xs.length - 1; i++) { const a = xs[i], b = xs[i + 1], m = (a + b) / 2; const vals = lines.map(f => ({ a: f(a), b: f(b), m: f(m) })) .sort((p, q) => p.m - q.m) .filter((v, k, arr) => k === 0 || v.m - arr[k - 1].m > eps || Math.abs(v.a - arr[k - 1].a) > eps || Math.abs(v.b - arr[k - 1].b) > eps); const cells = []; for (let k = 0; k < vals.length - 1; k++) { const lo = vals[k], hi = vals[k + 1]; if (hi.m - lo.m <= eps) continue; cells.push({ lo, hi, filled: !inRect(m, (lo.m + hi.m) / 2) }); } cols.push({ a, b, cells }); } const verts = []; const vid = (x, y) => { let k = verts.findIndex(v => Math.abs(v.x - x) < 1e-7 && Math.abs(v.y - y) < 1e-7); if (k < 0) { k = verts.length; verts.push({ x, y }); } return k; }; const edges = new Map(); const addEdge = (p, q) => { const a = vid(p.x, p.y), b = vid(q.x, q.y); if (a === b) return; if (!edges.has(a)) edges.set(a, []); edges.get(a).push(b); }; cols.forEach(col => col.cells.forEach((c, k) => { if (!c.filled) return; if (!(k > 0 && col.cells[k - 1].filled && col.cells[k - 1].hi === c.lo)) addEdge({ x: col.a, y: c.lo.a }, { x: col.b, y: c.lo.b }); if (!(k < col.cells.length - 1 && col.cells[k + 1].filled && col.cells[k + 1].lo === c.hi)) addEdge({ x: col.b, y: c.hi.b }, { x: col.a, y: c.hi.a }); })); for (let i = 0; i < xs.length; i++) { const x = xs[i]; const left = i > 0 ? cols[i - 1].cells.filter(c => c.filled).map(c => [c.lo.b, c.hi.b]) : []; const right = i < cols.length ? cols[i].cells.filter(c => c.filled).map(c => [c.lo.a, c.hi.a]) : []; const ys = uniq(left.concat(right).flat()); for (let k = 0; k < ys.length - 1; k++) { const ya = ys[k], yb = ys[k + 1], ym = (ya + yb) / 2; if (yb - ya <= eps) continue; const lf = left.some(([p, q]) => ym > p && ym < q), rf = right.some(([p, q]) => ym > p && ym < q); if (lf && !rf) addEdge({ x, y: ya }, { x, y: yb }); else if (rf && !lf) addEdge({ x, y: yb }, { x, y: ya }); } } const loops = []; edges.forEach((list, start) => { while (list.length) { const loop = [start]; let next = list.pop(), guard = 0; while (next !== start && guard++ < 100000) { loop.push(next); const outs = edges.get(next); if (!outs || !outs.length) break; next = outs.pop(); } if (next === start) loops.push(loop); } }); const polys = loops.map(loop => { const pts = loop.map(k => verts[k]); return pts.filter((p, k) => { const a = pts[(k - 1 + pts.length) % pts.length], b = pts[(k + 1) % pts.length]; return Math.abs((p.x - a.x) * (b.y - a.y) - (p.y - a.y) * (b.x - a.x)) > 1e-12; }); }); const signedArea = (pts) => pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + (p.x * q.y - q.x * p.y); }, 0) / 2; const inside = (pt, poly) => { let c = false; for (let a = 0, b = poly.length - 1; a < poly.length; b = a++) { if ((poly[a].y > pt.y) !== (poly[b].y > pt.y) && pt.x < (poly[b].x - poly[a].x) * (pt.y - poly[a].y) / (poly[b].y - poly[a].y) + poly[a].x) c = !c; } return c; }; const outers = polys.filter(p => p.length >= 3 && signedArea(p) > 1e-12).map(p => ({ outer: p, holes: [] })); outers.sort((p, q) => signedArea(p.outer) - signedArea(q.outer)); polys.filter(p => p.length >= 3 && signedArea(p) < -1e-12).forEach(hole => { const mid = { x: (hole[0].x + hole[1].x) / 2, y: (hole[0].y + hole[1].y) / 2 }; const owner = outers.find(o => inside(mid, o.outer)) || outers[0]; if (owner) owner.holes.push(hole); }); return outers; } function rectUnionLoops(rects) { const eps = 1e-6; const uniq = (arr) => arr.sort((a, b) => a - b).filter((v, i, a) => i === 0 || v - a[i - 1] > eps); const xs = uniq(rects.flatMap(r => [r.x0, r.x1])); const zs = uniq(rects.flatMap(r => [r.z0, r.z1])); const nx = xs.length - 1, nz = zs.length - 1; const filled = (i, j) => { if (i < 0 || j < 0 || i >= nx || j >= nz) return false; const xm = (xs[i] + xs[i + 1]) / 2, zm = (zs[j] + zs[j + 1]) / 2; return rects.some(r => xm > r.x0 && xm < r.x1 && zm > r.z0 && zm < r.z1); }; const edges = new Map(); const add = (a, b) => { const k = a.join(','); if (!edges.has(k)) edges.set(k, []); edges.get(k).push(b); }; for (let i = 0; i < nx; i++) { for (let j = 0; j < nz; j++) { if (!filled(i, j)) continue; if (!filled(i, j - 1)) add([i, j], [i + 1, j]); if (!filled(i + 1, j)) add([i + 1, j], [i + 1, j + 1]); if (!filled(i, j + 1)) add([i + 1, j + 1], [i, j + 1]); if (!filled(i - 1, j)) add([i, j + 1], [i, j]); } } const loops = []; edges.forEach((list, startKey) => { while (list.length) { const loop = [startKey.split(',').map(Number)]; let next = list.pop(); while (next.join(',') !== startKey) { loop.push(next); next = edges.get(next.join(',')).pop(); } const pts = loop.map(([i, j]) => ({ x: xs[i], z: zs[j] })); loops.push(pts.filter((p, k) => { const a = pts[(k - 1 + pts.length) % pts.length], b = pts[(k + 1) % pts.length]; return Math.abs((p.x - a.x) * (b.z - a.z) - (p.z - a.z) * (b.x - a.x)) > eps; })); } }); const area = (pts) => pts.reduce((s, p, k) => { const q = pts[(k + 1) % pts.length]; return s + p.x * q.z - q.x * p.z; }, 0) / 2; return loops.map(pts => ({ pts, area: area(pts) })).sort((a, b) => Math.abs(b.area) - Math.abs(a.area)); } function rectilinearNormal(a, b) { const dx = b.x - a.x, dz = b.z - a.z, len = Math.hypot(dx, dz) || 1; return { x: dz / len, z: -dx / len }; } function offsetRectilinearLoop(pts, d) { const n = pts.length; return pts.map((p, k) => { const prev = pts[(k - 1 + n) % n], next = pts[(k + 1) % n]; const n1 = rectilinearNormal(prev, p), n2 = rectilinearNormal(p, next); const same = Math.abs(n1.x - n2.x) < 1e-9 && Math.abs(n1.z - n2.z) < 1e-9; return same ? { x: p.x + n1.x * d, z: p.z + n1.z * d } : { x: p.x + (n1.x + n2.x) * d, z: p.z + (n1.z + n2.z) * d }; }); } function groupFacadeRuns(segs, opts) { const lineTol = (opts && opts.lineTol) || 1, gapTol = (opts && opts.gapTol) || 5; const runs = []; segs.slice().sort((a, b) => a.s0 - b.s0).forEach(sg => { const run = runs.find(r => r.axis === sg.axis && Math.abs(r.normal.x - sg.normal.x) < 1e-6 && Math.abs(r.normal.z - sg.normal.z) < 1e-6 && Math.abs(r.line - sg.line) < lineTol && sg.s0 <= r.s1 + gapTol); if (run) { run.segs.push(sg); run.s1 = Math.max(run.s1, sg.s1); } else runs.push(Object.assign({}, sg, { s0: sg.s0, s1: sg.s1, segs: [sg] })); }); return runs; }