v0.4.17: openingen met schuine bovenzijde (hoogte links/rechts, volg dakhelling), 9 sleeppunten in het vooraanzicht; regelwerk, beplating, doek, steen en latwerk volgen de schuinte (columnMinusRects met schuine uitsparingen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
23bfe153a2
commit
8cdfa21f46
+100
-58
@@ -154,87 +154,126 @@ function summarizeStockPlan(bars) {
|
||||
}
|
||||
|
||||
// Verticale strook [x0, x1] van y = yBottom tot een (evt. schuine, lineaire)
|
||||
// bovenrand topYAt(x), MIN een set rechthoekige uitsparingen
|
||||
// ({ x0, x1, y0, y1 }). Uitsparingen die over de rand van de strook vallen
|
||||
// worden een echte inkeping in de omtrek (i.p.v. een gat dat net binnen de
|
||||
// rand wordt afgekapt — dat liet een haarfijn reepje materiaal met zichtbare
|
||||
// randlijnen door de opening lopen). Een uitsparing over de volle breedte
|
||||
// splitst de strook in losse delen.
|
||||
// bovenrand topYAt(x), MIN een set uitsparingen ({ x0, x1, y0, y1, y1r? }):
|
||||
// rechthoeken, of met y1r een schuine bovenkant (lineair van (x0, y1) naar
|
||||
// (x1, y1r); schuin bovenin mag, onderin niet). Uitsparingen die over de
|
||||
// rand van de strook vallen worden een echte inkeping in de omtrek (i.p.v.
|
||||
// een gat dat net binnen de rand wordt afgekapt — dat liet een haarfijn
|
||||
// reepje materiaal met zichtbare randlijnen door de opening lopen). Een
|
||||
// uitsparing over de volle breedte splitst de strook in losse delen; een
|
||||
// uitsparing tot in de (schuine) bovenrand wordt een inkeping in de bovenrand.
|
||||
// opts (optioneel):
|
||||
// yCap — vlakke bovengrens (bijv. bovenkant van een vochtdoek-baan);
|
||||
// topYAt mag dan min(yCap, daklijn) zijn. Uitsparingen die tot
|
||||
// boven yCap reiken worden er netjes op afgekapt (inkeping in de
|
||||
// bovenrand).
|
||||
// extraXs — knikpunten van topYAt (waar de daklijn yCap kruist), zodat de
|
||||
// bovenrand tussen twee hoekpunten steeds lineair is.
|
||||
// Resultaat: [{ outer: [{x,y}...] (CCW), holes: [[{x,y}...] (CW)] }], of null
|
||||
// als een uitsparing tot in de schuine bovenrand reikt (niet ondersteund).
|
||||
// yCap — vlakke bovengrens (bijv. bovenkant van een vochtdoek-baan).
|
||||
// extraXs — knikpunten van topYAt (bijv. waar de daklijn yCap kruist), zodat
|
||||
// de bovenrand tussen twee hoekpunten steeds lineair is.
|
||||
// Werkwijze: de strook wordt in kolommen geknipt (op alle x-randen, knikken
|
||||
// en snijpunten van de lijnen), zodat binnen een kolom elke lijn (onderkant,
|
||||
// onder-/bovenkant uitsparing, bovenrand) lineair is en ze elkaar niet
|
||||
// kruisen; de cellen tussen twee lijnen zijn vol of leeg, en de randen
|
||||
// tussen vol en leeg worden tot lussen aaneengeregen.
|
||||
// Resultaat: [{ outer: [{x,y}...] (CCW), holes: [[{x,y}...] (CW)] }].
|
||||
function columnMinusRects(x0, x1, yBottom, topYAt, rects, opts) {
|
||||
const eps = 1e-9;
|
||||
const yCap = (opts && opts.yCap !== undefined) ? opts.yCap : Infinity;
|
||||
const extraXs = ((opts && opts.extraXs) || []).filter(x => x > x0 + eps && x < x1 - eps);
|
||||
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
|
||||
.map(r => ({ x0: Math.max(x0, r.x0), x1: Math.min(x1, r.x1), y0: Math.max(yBottom, r.y0), y1: Math.min(yCap, r.y1) }))
|
||||
.filter(r => r.x1 - r.x0 > eps && r.y1 - r.y0 > eps);
|
||||
const xs = uniq([x0, x1, ...extraXs, ...rs.flatMap(r => [r.x0, r.x1])]);
|
||||
.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));
|
||||
|
||||
// Laagste bovenrand binnen [a, b] (topYAt is lineair tussen de knikpunten).
|
||||
const minTopIn = (a, b) => Math.min(...xs.filter(x => x >= a - eps && x <= b + eps).map(topYAt));
|
||||
const reachesTop = (r) => r.y1 >= yCap - eps && minTopIn(r.x0, r.x1) >= yCap - eps; // tot de vlakke bovenrand
|
||||
if (rs.some(r => !reachesTop(r) && r.y1 >= minTopIn(r.x0, r.x1) - eps)) return null;
|
||||
// Lijnen (ongeklemd); geklemd tussen yBottom en de bovenrand.
|
||||
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);
|
||||
|
||||
const ys = uniq([yBottom, ...rs.flatMap(r => reachesTop(r) ? [r.y0] : [r.y0, r.y1]).filter(y => y > yBottom + eps)]);
|
||||
const nCols = xs.length - 1, nRows = ys.length; // bovenste rij loopt tot de daklijn
|
||||
let xs = uniq([x0, x1, ...((opts && opts.extraXs) || []).filter(x => x > x0 + eps && x < x1 - eps), ...rs.flatMap(r => [r.x0, r.x1])]);
|
||||
// Snijpunten van de lijnen (ook met de bovenrand/onderkant) als extra kolomranden.
|
||||
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 vertex = (i, j) => ({ x: xs[i], y: j < nRows ? ys[j] : topYAt(xs[i]) });
|
||||
const filled = (i, j) => {
|
||||
if (i < 0 || i >= nCols || j < 0 || j >= nRows) return false;
|
||||
const xm = (xs[i] + xs[i + 1]) / 2;
|
||||
const ym = j < nRows - 1 ? (ys[j] + ys[j + 1]) / 2 : (ys[j] + topYAt(xm)) / 2;
|
||||
return !rs.some(r => xm > r.x0 && xm < r.x1 && ym > r.y0 && ym < r.y1);
|
||||
// Per kolom: cellen tussen opeenvolgende (verschillende) lijnen.
|
||||
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 });
|
||||
}
|
||||
|
||||
// Hoekpunten gedeeld via een register (bijna-gelijke punten = hetzelfde punt).
|
||||
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;
|
||||
};
|
||||
|
||||
// Gerichte randen van gevulde cellen (CCW), alleen waar de buur leeg is.
|
||||
const edges = new Map(); // "i,j" -> [[i2,j2], ...]
|
||||
const addEdge = (a, b) => {
|
||||
const key = a.join(',');
|
||||
if (!edges.has(key)) edges.set(key, []);
|
||||
edges.get(key).push(b);
|
||||
const edges = new Map(); // van -> [naar, ...]
|
||||
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);
|
||||
};
|
||||
for (let i = 0; i < nCols; i++) {
|
||||
for (let j = 0; j < nRows; j++) {
|
||||
if (!filled(i, j)) continue;
|
||||
if (!filled(i, j - 1)) addEdge([i, j], [i + 1, j]);
|
||||
if (!filled(i + 1, j)) addEdge([i + 1, j], [i + 1, j + 1]);
|
||||
if (!filled(i, j + 1)) addEdge([i + 1, j + 1], [i, j + 1]);
|
||||
if (!filled(i - 1, j)) addEdge([i, j + 1], [i, j]);
|
||||
// Onder-/bovenranden binnen een kolom (CCW: onder naar rechts, boven naar links).
|
||||
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 });
|
||||
}));
|
||||
// Verticale randen op elke kolomgrens: waar links vol en rechts leeg is (of andersom).
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// Randen aaneenrijgen tot gesloten lussen (collineaire tussenpunten weg).
|
||||
const loops = [];
|
||||
edges.forEach((list, startKey) => {
|
||||
edges.forEach((list, start) => {
|
||||
while (list.length) {
|
||||
const loop = [];
|
||||
let cur = startKey.split(',').map(Number);
|
||||
let next = list.pop();
|
||||
loop.push(cur);
|
||||
while (next.join(',') !== startKey) {
|
||||
const loop = [start];
|
||||
let next = list.pop(), guard = 0;
|
||||
while (next !== start && guard++ < 100000) {
|
||||
loop.push(next);
|
||||
cur = next;
|
||||
const outs = edges.get(cur.join(','));
|
||||
const outs = edges.get(next);
|
||||
if (!outs || !outs.length) break;
|
||||
next = outs.pop();
|
||||
}
|
||||
loops.push(loop);
|
||||
if (next === start) loops.push(loop);
|
||||
}
|
||||
});
|
||||
const polys = loops.map(loop => {
|
||||
// Eerst dubbele punten weg (bijv. een spits toelopend uiteinde waar de
|
||||
// bovenrand de onderrand raakt), dan collineaire tussenpunten.
|
||||
const pts = loop.map(([i, j]) => vertex(i, j))
|
||||
.filter((p, k, arr) => { const q = arr[(k + 1) % arr.length]; return Math.hypot(p.x - q.x, p.y - q.y) > 1e-9; });
|
||||
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;
|
||||
@@ -254,8 +293,11 @@ function columnMinusRects(x0, x1, yBottom, topYAt, rects, opts) {
|
||||
return c;
|
||||
};
|
||||
const outers = polys.filter(p => p.length >= 3 && signedArea(p) > 1e-12).map(p => ({ outer: p, holes: [] }));
|
||||
// Gat bij de kleinste buitenlus die het bevat.
|
||||
outers.sort((p, q) => signedArea(p.outer) - signedArea(q.outer));
|
||||
polys.filter(p => p.length >= 3 && signedArea(p) < -1e-12).forEach(hole => {
|
||||
const owner = outers.find(o => inside(hole[0], o.outer)) || outers[0];
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user