Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cdfa21f46 |
@@ -654,3 +654,10 @@ h1 { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight:
|
|||||||
#app-bar .submenu .menu-info { padding: 6px 12px 2px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted, #8a8f96); }
|
#app-bar .submenu .menu-info { padding: 6px 12px 2px; font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted, #8a8f96); }
|
||||||
#app-bar .submenu .menu-help { cursor: default; opacity: .85; }
|
#app-bar .submenu .menu-help { cursor: default; opacity: .85; }
|
||||||
#app-bar .submenu .menu-help:hover { background: transparent; color: var(--nav-link-color); }
|
#app-bar .submenu .menu-help:hover { background: transparent; color: var(--nav-link-color); }
|
||||||
|
|
||||||
|
/* Openingen: "Volg dakhelling" (schuine bovenzijde) */
|
||||||
|
.opening-roof-btn {
|
||||||
|
display: inline-flex; align-items: center; gap: 5px; margin-top: 2px; padding: 4px 8px; font-size: 11.5px;
|
||||||
|
background: var(--surface); border: 1px solid var(--border); color: var(--primary); border-radius: 4px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.opening-roof-btn:hover { background: var(--primary-soft); border-color: var(--primary); }
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||||
* Zie LICENTIE.md.
|
* Zie LICENTIE.md.
|
||||||
*/
|
*/
|
||||||
const APP_VERSION = '0.4.16';
|
const APP_VERSION = '0.4.17';
|
||||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||||
|
|
||||||
let tabs = [];
|
let tabs = [];
|
||||||
|
|||||||
+100
-58
@@ -154,87 +154,126 @@ function summarizeStockPlan(bars) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verticale strook [x0, x1] van y = yBottom tot een (evt. schuine, lineaire)
|
// Verticale strook [x0, x1] van y = yBottom tot een (evt. schuine, lineaire)
|
||||||
// bovenrand topYAt(x), MIN een set rechthoekige uitsparingen
|
// bovenrand topYAt(x), MIN een set uitsparingen ({ x0, x1, y0, y1, y1r? }):
|
||||||
// ({ x0, x1, y0, y1 }). Uitsparingen die over de rand van de strook vallen
|
// rechthoeken, of met y1r een schuine bovenkant (lineair van (x0, y1) naar
|
||||||
// worden een echte inkeping in de omtrek (i.p.v. een gat dat net binnen de
|
// (x1, y1r); schuin bovenin mag, onderin niet). Uitsparingen die over de
|
||||||
// rand wordt afgekapt — dat liet een haarfijn reepje materiaal met zichtbare
|
// rand van de strook vallen worden een echte inkeping in de omtrek (i.p.v.
|
||||||
// randlijnen door de opening lopen). Een uitsparing over de volle breedte
|
// een gat dat net binnen de rand wordt afgekapt — dat liet een haarfijn
|
||||||
// splitst de strook in losse delen.
|
// 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):
|
// opts (optioneel):
|
||||||
// yCap — vlakke bovengrens (bijv. bovenkant van een vochtdoek-baan);
|
// yCap — vlakke bovengrens (bijv. bovenkant van een vochtdoek-baan).
|
||||||
// topYAt mag dan min(yCap, daklijn) zijn. Uitsparingen die tot
|
// extraXs — knikpunten van topYAt (bijv. waar de daklijn yCap kruist), zodat
|
||||||
// boven yCap reiken worden er netjes op afgekapt (inkeping in de
|
// de bovenrand tussen twee hoekpunten steeds lineair is.
|
||||||
// bovenrand).
|
// Werkwijze: de strook wordt in kolommen geknipt (op alle x-randen, knikken
|
||||||
// extraXs — knikpunten van topYAt (waar de daklijn yCap kruist), zodat de
|
// en snijpunten van de lijnen), zodat binnen een kolom elke lijn (onderkant,
|
||||||
// bovenrand tussen twee hoekpunten steeds lineair is.
|
// onder-/bovenkant uitsparing, bovenrand) lineair is en ze elkaar niet
|
||||||
// Resultaat: [{ outer: [{x,y}...] (CCW), holes: [[{x,y}...] (CW)] }], of null
|
// kruisen; de cellen tussen twee lijnen zijn vol of leeg, en de randen
|
||||||
// als een uitsparing tot in de schuine bovenrand reikt (niet ondersteund).
|
// 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) {
|
function columnMinusRects(x0, x1, yBottom, topYAt, rects, opts) {
|
||||||
const eps = 1e-9;
|
const eps = 1e-9;
|
||||||
const yCap = (opts && opts.yCap !== undefined) ? opts.yCap : Infinity;
|
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 uniq = (arr) => arr.sort((a, b) => a - b).filter((v, i, a) => i === 0 || v - a[i - 1] > eps);
|
||||||
|
|
||||||
const rs = rects
|
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 => Math.min(x1, r.x1) - Math.max(x0, r.x0) > eps)
|
||||||
.filter(r => r.x1 - r.x0 > eps && r.y1 - r.y0 > eps);
|
.map(r => {
|
||||||
const xs = uniq([x0, x1, ...extraXs, ...rs.flatMap(r => [r.x0, r.x1])]);
|
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).
|
// Lijnen (ongeklemd); geklemd tussen yBottom en de bovenrand.
|
||||||
const minTopIn = (a, b) => Math.min(...xs.filter(x => x >= a - eps && x <= b + eps).map(topYAt));
|
const raw = [() => yBottom, top];
|
||||||
const reachesTop = (r) => r.y1 >= yCap - eps && minTopIn(r.x0, r.x1) >= yCap - eps; // tot de vlakke bovenrand
|
rs.forEach(r => raw.push(() => r.y0, r.topAt));
|
||||||
if (rs.some(r => !reachesTop(r) && r.y1 >= minTopIn(r.x0, r.x1) - eps)) return null;
|
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)]);
|
let xs = uniq([x0, x1, ...((opts && opts.extraXs) || []).filter(x => x > x0 + eps && x < x1 - eps), ...rs.flatMap(r => [r.x0, r.x1])]);
|
||||||
const nCols = xs.length - 1, nRows = ys.length; // bovenste rij loopt tot de daklijn
|
// 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]) });
|
// Per kolom: cellen tussen opeenvolgende (verschillende) lijnen.
|
||||||
const filled = (i, j) => {
|
const cols = [];
|
||||||
if (i < 0 || i >= nCols || j < 0 || j >= nRows) return false;
|
for (let i = 0; i < xs.length - 1; i++) {
|
||||||
const xm = (xs[i] + xs[i + 1]) / 2;
|
const a = xs[i], b = xs[i + 1], m = (a + b) / 2;
|
||||||
const ym = j < nRows - 1 ? (ys[j] + ys[j + 1]) / 2 : (ys[j] + topYAt(xm)) / 2;
|
const vals = lines.map(f => ({ a: f(a), b: f(b), m: f(m) }))
|
||||||
return !rs.some(r => xm > r.x0 && xm < r.x1 && ym > r.y0 && ym < r.y1);
|
.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;
|
||||||
};
|
};
|
||||||
|
const edges = new Map(); // van -> [naar, ...]
|
||||||
// Gerichte randen van gevulde cellen (CCW), alleen waar de buur leeg is.
|
const addEdge = (p, q) => {
|
||||||
const edges = new Map(); // "i,j" -> [[i2,j2], ...]
|
const a = vid(p.x, p.y), b = vid(q.x, q.y);
|
||||||
const addEdge = (a, b) => {
|
if (a === b) return;
|
||||||
const key = a.join(',');
|
if (!edges.has(a)) edges.set(a, []);
|
||||||
if (!edges.has(key)) edges.set(key, []);
|
edges.get(a).push(b);
|
||||||
edges.get(key).push(b);
|
|
||||||
};
|
};
|
||||||
for (let i = 0; i < nCols; i++) {
|
// Onder-/bovenranden binnen een kolom (CCW: onder naar rechts, boven naar links).
|
||||||
for (let j = 0; j < nRows; j++) {
|
cols.forEach(col => col.cells.forEach((c, k) => {
|
||||||
if (!filled(i, j)) continue;
|
if (!c.filled) return;
|
||||||
if (!filled(i, j - 1)) addEdge([i, j], [i + 1, j]);
|
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 (!filled(i + 1, j)) addEdge([i + 1, j], [i + 1, j + 1]);
|
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 });
|
||||||
if (!filled(i, j + 1)) addEdge([i + 1, j + 1], [i, j + 1]);
|
}));
|
||||||
if (!filled(i - 1, j)) addEdge([i, j + 1], [i, j]);
|
// 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).
|
// Randen aaneenrijgen tot gesloten lussen (collineaire tussenpunten weg).
|
||||||
const loops = [];
|
const loops = [];
|
||||||
edges.forEach((list, startKey) => {
|
edges.forEach((list, start) => {
|
||||||
while (list.length) {
|
while (list.length) {
|
||||||
const loop = [];
|
const loop = [start];
|
||||||
let cur = startKey.split(',').map(Number);
|
let next = list.pop(), guard = 0;
|
||||||
let next = list.pop();
|
while (next !== start && guard++ < 100000) {
|
||||||
loop.push(cur);
|
|
||||||
while (next.join(',') !== startKey) {
|
|
||||||
loop.push(next);
|
loop.push(next);
|
||||||
cur = next;
|
const outs = edges.get(next);
|
||||||
const outs = edges.get(cur.join(','));
|
if (!outs || !outs.length) break;
|
||||||
next = outs.pop();
|
next = outs.pop();
|
||||||
}
|
}
|
||||||
loops.push(loop);
|
if (next === start) loops.push(loop);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const polys = loops.map(loop => {
|
const polys = loops.map(loop => {
|
||||||
// Eerst dubbele punten weg (bijv. een spits toelopend uiteinde waar de
|
const pts = loop.map(k => verts[k]);
|
||||||
// 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; });
|
|
||||||
return pts.filter((p, k) => {
|
return pts.filter((p, k) => {
|
||||||
const a = pts[(k - 1 + pts.length) % pts.length], b = pts[(k + 1) % pts.length];
|
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;
|
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;
|
return c;
|
||||||
};
|
};
|
||||||
const outers = polys.filter(p => p.length >= 3 && signedArea(p) > 1e-12).map(p => ({ outer: p, holes: [] }));
|
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 => {
|
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);
|
if (owner) owner.holes.push(hole);
|
||||||
});
|
});
|
||||||
return outers;
|
return outers;
|
||||||
|
|||||||
@@ -681,8 +681,10 @@ window.HouseModel = (() => {
|
|||||||
const ops = firstConfig.openings.map(op => {
|
const ops = firstConfig.openings.map(op => {
|
||||||
const wa = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x, 0);
|
const wa = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x, 0);
|
||||||
const wb = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x + op.w, 0);
|
const wb = UnitModel.localToWorld(first.place, -first.place.width / 2 + op.x + op.w, 0);
|
||||||
const u = Math.min(toLocalU(lf.place, wa), toLocalU(lf.place, wb));
|
const ua = toLocalU(lf.place, wa), ub = toLocalU(lf.place, wb);
|
||||||
return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: Math.round(u) };
|
// Gespiegeld (andere kijkrichting): schuine bovenzijde klapt om.
|
||||||
|
const flip = ua > ub;
|
||||||
|
return { id: op.id, type: op.type, w: op.w, h: flip ? op.hR : op.h, hR: flip ? op.h : op.hR, y: op.y, anchor: 'left', offset: Math.round(Math.min(ua, ub)) };
|
||||||
});
|
});
|
||||||
config = WallModel.buildConfig(paramsOf(lf), ops);
|
config = WallModel.buildConfig(paramsOf(lf), ops);
|
||||||
}
|
}
|
||||||
@@ -827,11 +829,11 @@ window.HouseModel = (() => {
|
|||||||
const poly = [{ x: -W / 2, y: y0 }, { x: W / 2, y: y0 }, ...tl.pts.slice().reverse()];
|
const poly = [{ x: -W / 2, y: y0 }, { x: W / 2, y: y0 }, ...tl.pts.slice().reverse()];
|
||||||
return Math.max(0, polygonArea(poly)) + (p.skinAbove || 0) * W;
|
return Math.max(0, polygonArea(poly)) + (p.skinAbove || 0) * W;
|
||||||
};
|
};
|
||||||
const openingsMM2 = (p) => (p.config.openings || []).reduce((a, op) => a + op.w * op.h, 0);
|
const openingsMM2 = (p) => (p.config.openings || []).reduce((a, op) => a + WallModel.openingArea(op), 0);
|
||||||
const facadePieces = ext.concat(parts.interior.filter(p => p.skinFrom !== undefined));
|
const facadePieces = ext.concat(parts.interior.filter(p => p.skinFrom !== undefined));
|
||||||
const gevelBruto = facadePieces.reduce((a, p) => a + wallArea(p, p.skinFrom || 0), 0);
|
const gevelBruto = facadePieces.reduce((a, p) => a + wallArea(p, p.skinFrom || 0), 0);
|
||||||
const gevelOpen = ext.reduce((a, p) => a + openingsMM2(p), 0);
|
const gevelOpen = ext.reduce((a, p) => a + openingsMM2(p), 0);
|
||||||
const glas = ext.reduce((a, p) => a + (p.config.openings || []).filter(op => op.type === 'raam').reduce((b, op) => b + op.w * op.h, 0), 0);
|
const glas = ext.reduce((a, p) => a + (p.config.openings || []).filter(op => op.type === 'raam').reduce((b, op) => b + WallModel.openingArea(op), 0), 0);
|
||||||
const binnen = all.reduce((a, p) => a + ((p.stats && p.stats.spaanplaatAreaM2) || 0) + ((p.stats && p.stats.spaanplaatBuitenAreaM2) || 0), 0);
|
const binnen = all.reduce((a, p) => a + ((p.stats && p.stats.spaanplaatAreaM2) || 0) + ((p.stats && p.stats.spaanplaatBuitenAreaM2) || 0), 0);
|
||||||
const dakM2 = parts.units.reduce((a, u) => a + (u.roof.specs.dakleerM2 || 0) + (u.roof.specs.rietM2 || 0), 0);
|
const dakM2 = parts.units.reduce((a, u) => a + (u.roof.specs.dakleerM2 || 0) + (u.roof.specs.rietM2 || 0), 0);
|
||||||
const dakPlanMM2 = parts.units.reduce((a, u) => a + u.roof.rect.W * u.roof.rect.D, 0);
|
const dakPlanMM2 = parts.units.reduce((a, u) => a + u.roof.rect.W * u.roof.rect.D, 0);
|
||||||
|
|||||||
@@ -229,10 +229,12 @@ window.HousePdf = (() => {
|
|||||||
}
|
}
|
||||||
// Openingen (glas / deur), vóór de gevel
|
// Openingen (glas / deur), vóór de gevel
|
||||||
(p.config.openings || []).forEach(op => {
|
(p.config.openings || []).forEach(op => {
|
||||||
if (op.y + op.h <= y0) return;
|
if (WallModel.openingTopMax(op) <= y0) return;
|
||||||
const x0 = -p.place.width / 2 + op.x, x1 = x0 + op.w, ya = Math.max(y0, op.y), yb = op.y + op.h;
|
// Schuine bovenzijde: bovenkant links/rechts.
|
||||||
|
const x0 = -p.place.width / 2 + op.x, x1 = x0 + op.w, ya = Math.max(y0, op.y);
|
||||||
|
const ybL = Math.max(ya, op.y + op.h), ybR = Math.max(ya, op.y + op.hR);
|
||||||
const color = op.type === 'raam' ? VIEW_COLORS.glas : VIEW_COLORS.deur;
|
const color = op.type === 'raam' ? VIEW_COLORS.glas : VIEW_COLORS.deur;
|
||||||
faces.push({ pts: [[x0, ya], [x1, ya], [x1, yb], [x0, yb]].map(([lx, ly]) => toWorld(p, lx, ly, zOut)), n, color, bias: 2, window: true });
|
faces.push({ pts: [[x0, ya], [x1, ya], [x1, ybR], [x0, ybL]].map(([lx, ly]) => toWorld(p, lx, ly, zOut)), n, color, bias: 2, window: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
parts.units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => addWall(p, 0))));
|
parts.units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => addWall(p, 0))));
|
||||||
|
|||||||
+3
-3
@@ -350,8 +350,8 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
}
|
}
|
||||||
const e = p.skinExt || {};
|
const e = p.skinExt || {};
|
||||||
const xL = -W / 2 - (e.left > 0 ? e.left + out : 0), xR = W / 2 + (e.right > 0 ? e.right + out : 0);
|
const xL = -W / 2 - (e.left > 0 ? e.left + out : 0), xR = W / 2 + (e.right > 0 ? e.right + out : 0);
|
||||||
const gaps = p.config.openings.filter(op => op.y < y && op.y + op.h > y)
|
const gaps = p.config.openings.map(op => WallModel.openingSpanAtY(op, y)).filter(Boolean)
|
||||||
.map(op => [-W / 2 + op.x, -W / 2 + op.x + op.w]).sort((a, b) => a[0] - b[0]);
|
.map(([a, b]) => [-W / 2 + a, -W / 2 + b]).sort((a, b) => a[0] - b[0]);
|
||||||
const spans = [];
|
const spans = [];
|
||||||
let cur = xL;
|
let cur = xL;
|
||||||
gaps.forEach(([a, b]) => { if (a > cur) spans.push([cur, Math.min(a, xR)]); cur = Math.max(cur, b); });
|
gaps.forEach(([a, b]) => { if (a > cur) spans.push([cur, Math.min(a, xR)]); cur = Math.max(cur, b); });
|
||||||
@@ -1118,7 +1118,7 @@ function mountHouseEditor(container, callbacks) {
|
|||||||
const out = p.place.normal ? WallModel.claddingOffset(p.buitenMateriaal, p.stone) : 0;
|
const out = p.place.normal ? WallModel.claddingOffset(p.buitenMateriaal, p.stone) : 0;
|
||||||
if (lz < -T / 2 - WALK_RADIUS || lz > T / 2 + out + WALK_RADIUS || Math.abs(lx) > W / 2 + WALK_RADIUS) return false;
|
if (lz < -T / 2 - WALK_RADIUS || lz > T / 2 + out + WALK_RADIUS || Math.abs(lx) > W / 2 + WALK_RADIUS) return false;
|
||||||
// Door een deur(opening) mag je lopen.
|
// Door een deur(opening) mag je lopen.
|
||||||
return !(p.config.openings || []).some(op => op.y < 100 && op.y + op.h > 1800 &&
|
return !(p.config.openings || []).some(op => op.y < 100 && op.y + Math.min(op.h, op.hR) > 1800 &&
|
||||||
lx > -W / 2 + op.x + WALK_RADIUS * 0.6 && lx < -W / 2 + op.x + op.w - WALK_RADIUS * 0.6);
|
lx > -W / 2 + op.x + WALK_RADIUS * 0.6 && lx < -W / 2 + op.x + op.w - WALK_RADIUS * 0.6);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ window.LightingModel = (() => {
|
|||||||
.filter(u => u >= 0 && u <= cfg.wallWidth).forEach(u => pos.push(uToS(sg, u))));
|
.filter(u => u >= 0 && u <= cfg.wallWidth).forEach(u => pos.push(uToS(sg, u))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cfg.openings.filter(op => op.y - sW <= H && op.y + op.h + sW >= H).forEach(op => {
|
cfg.openings.filter(op => op.y - sW <= H && WallModel.openingTopMax(op) + sW >= H).forEach(op => {
|
||||||
const a = uToS(sg, op.x - sW), b = uToS(sg, op.x + op.w + sW);
|
const a = uToS(sg, op.x - sW), b = uToS(sg, op.x + op.w + sW);
|
||||||
blocked.push([Math.min(a, b), Math.max(a, b)]);
|
blocked.push([Math.min(a, b), Math.max(a, b)]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ window.UnitModel = (() => {
|
|||||||
const pieces = wallLayout.pieces.map(([a, b, inL, inR], i) => {
|
const pieces = wallLayout.pieces.map(([a, b, inL, inR], i) => {
|
||||||
const openings = fullConfig.openings
|
const openings = fullConfig.openings
|
||||||
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS)
|
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS)
|
||||||
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y, anchor: 'left', offset: op.x - a }; });
|
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, hR: op.hR, y: op.y, anchor: 'left', offset: op.x - a }; });
|
||||||
const hasPeak = hM !== null && a < pU - 1 && b > pU + 1;
|
const hasPeak = hM !== null && a < pU - 1 && b > pU + 1;
|
||||||
const params = Object.assign({}, wallState.wall, {
|
const params = Object.assign({}, wallState.wall, {
|
||||||
width: b - a, heightLeft: Math.round(heightAt(a)), heightRight: Math.round(heightAt(b)),
|
width: b - a, heightLeft: Math.round(heightAt(a)), heightRight: Math.round(heightAt(b)),
|
||||||
@@ -233,7 +233,7 @@ window.UnitModel = (() => {
|
|||||||
if (Math.min(hA, hB) < MIN_RAISE_MM) return;
|
if (Math.min(hA, hB) < MIN_RAISE_MM) return;
|
||||||
const openings = fullConfig.openings
|
const openings = fullConfig.openings
|
||||||
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS && op.y >= base - EPS)
|
.filter(op => op.x >= a - EPS && op.x + op.w <= b + EPS && op.y >= base - EPS)
|
||||||
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, y: op.y - base, anchor: 'left', offset: op.x - a }; });
|
.map(op => { used.add(op.id); return { id: op.id, type: op.type, w: op.w, h: op.h, hR: op.hR, y: op.y - base, anchor: 'left', offset: op.x - a }; });
|
||||||
const params = Object.assign({}, wallState.wall, {
|
const params = Object.assign({}, wallState.wall, {
|
||||||
width: b - a, heightLeft: Math.round(hA), heightRight: Math.round(hB),
|
width: b - a, heightLeft: Math.round(hA), heightRight: Math.round(hB),
|
||||||
heightMid: hasPeak ? hM - base : null, peakX: hasPeak ? pU - a : undefined
|
heightMid: hasPeak ? hM - base : null, peakX: hasPeak ? pU - a : undefined
|
||||||
|
|||||||
+137
-133
@@ -15,7 +15,7 @@
|
|||||||
//
|
//
|
||||||
// Werkt met een "wand-state" (zelfde formaat als getStateJson/JSON-export):
|
// Werkt met een "wand-state" (zelfde formaat als getStateJson/JSON-export):
|
||||||
// { wall: {width, heightAll, heightLeft, heightRight, heightMid?, studWidth, ...},
|
// { wall: {width, heightAll, heightLeft, heightRight, heightMid?, studWidth, ...},
|
||||||
// materials: {interior, exterior}, openings: [{id,w,h,y,anchor,offset}] }
|
// materials: {interior, exterior}, openings: [{id,w,h,hR?,y,anchor,offset}] }
|
||||||
//
|
//
|
||||||
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
|
// Plain script (geen ES-module), moet na core/geometry.js geladen worden.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -173,6 +173,24 @@ window.WallModel = (() => {
|
|||||||
// koningsstijl (stevigheid, computeFrame).
|
// koningsstijl (stevigheid, computeFrame).
|
||||||
const isDoor = (op) => op.type === 'deur' || op.type === 'voordeur';
|
const isDoor = (op) => op.type === 'deur' || op.type === 'voordeur';
|
||||||
|
|
||||||
|
// Schuine bovenzijde (zoals de wand zelf: schuin bovenin mag, onderin
|
||||||
|
// niet): h = hoogte links, hR = hoogte rechts. Ruwe openingen hebben hR
|
||||||
|
// alleen als hij (bewust) schuin is; opgeloste openingen altijd.
|
||||||
|
// x is lokaal (0..wandbreedte), zoals op.x.
|
||||||
|
const hRightOf = (op) => op.hR !== undefined ? op.hR : op.h;
|
||||||
|
const openingTopAt = (op, x) => op.y + op.h + (x - op.x) / op.w * (hRightOf(op) - op.h);
|
||||||
|
const openingTopMax = (op) => op.y + Math.max(op.h, hRightOf(op));
|
||||||
|
const openingArea = (op) => op.w * (op.h + hRightOf(op)) / 2;
|
||||||
|
// Stuk [x0, x1] van een vlak met waterpas onderkant y0 en schuine
|
||||||
|
// bovenkant (yl links, yr rechts) op hoogte y, of null.
|
||||||
|
function slopedSpanAtY(x0, x1, y0, yl, yr, y) {
|
||||||
|
if (y <= y0 || y >= Math.max(yl, yr)) return null;
|
||||||
|
if (y <= Math.min(yl, yr)) return [x0, x1];
|
||||||
|
const xc = x0 + (y - yl) / (yr - yl) * (x1 - x0);
|
||||||
|
return yr > yl ? [xc, x1] : [x0, xc];
|
||||||
|
}
|
||||||
|
const openingSpanAtY = (op, y) => slopedSpanAtY(op.x, op.x + op.w, op.y, op.y + op.h, op.y + hRightOf(op), y);
|
||||||
|
|
||||||
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
// Buitenzijde-materialen (sleutel -> weergavenaam). 'spaanplaat' is voor
|
||||||
// binnenwanden (aan beide zijden spaanplaat).
|
// binnenwanden (aan beide zijden spaanplaat).
|
||||||
// 'geen': geen beplating aan de buitenzijde — naadwand-deel dat tegen het
|
// 'geen': geen beplating aan de buitenzijde — naadwand-deel dat tegen het
|
||||||
@@ -287,7 +305,7 @@ window.WallModel = (() => {
|
|||||||
else if (raw.anchor === 'middle') x = (wallWidth - w) / 2 + raw.offset;
|
else if (raw.anchor === 'middle') x = (wallWidth - w) / 2 + raw.offset;
|
||||||
else x = raw.offset;
|
else x = raw.offset;
|
||||||
|
|
||||||
return { id: raw.id, type: raw.type || 'raam', w, anchor: raw.anchor, offset: raw.offset, x, rawH: raw.h, rawY: raw.y };
|
return { id: raw.id, type: raw.type || 'raam', w, anchor: raw.anchor, offset: raw.offset, x, rawH: raw.h, rawHR: hRightOf(raw), rawY: raw.y };
|
||||||
});
|
});
|
||||||
|
|
||||||
resolved.sort((a, b) => a.x - b.x);
|
resolved.sort((a, b) => a.x - b.x);
|
||||||
@@ -301,10 +319,11 @@ window.WallModel = (() => {
|
|||||||
// 2x stijldikte ertussen) moeten naast elkaar; staan ze boven elkaar
|
// 2x stijldikte ertussen) moeten naast elkaar; staan ze boven elkaar
|
||||||
// (bijv. een raam boven een deur), dan mogen ze in x overlappen.
|
// (bijv. een raam boven een deur), dan mogen ze in x overlappen.
|
||||||
// Hoogte hier op basis van de invoer; na fase 2 wordt het nog gecontroleerd.
|
// Hoogte hier op basis van de invoer; na fase 2 wordt het nog gecontroleerd.
|
||||||
|
// (Schuine bovenzijde: de hoogste kant telt.)
|
||||||
const vBand = (o, h, y) => [y > plateThick ? y - studWidth : 0, y + h + studWidth];
|
const vBand = (o, h, y) => [y > plateThick ? y - studWidth : 0, y + h + studWidth];
|
||||||
const stacked = (a, b, useFinal) => {
|
const stacked = (a, b, useFinal) => {
|
||||||
const [a0, a1] = useFinal ? vBand(a, a.h, a.y) : vBand(a, Math.max(400, a.rawH), a.rawY);
|
const [a0, a1] = useFinal ? vBand(a, Math.max(a.h, a.hR), a.y) : vBand(a, Math.max(400, a.rawH, a.rawHR), a.rawY);
|
||||||
const [b0, b1] = useFinal ? vBand(b, b.h, b.y) : vBand(b, Math.max(400, b.rawH), b.rawY);
|
const [b0, b1] = useFinal ? vBand(b, Math.max(b.h, b.hR), b.y) : vBand(b, Math.max(400, b.rawH, b.rawHR), b.rawY);
|
||||||
return a1 <= b0 + 0.5 || b1 <= a0 + 0.5;
|
return a1 <= b0 + 0.5 || b1 <= a0 + 0.5;
|
||||||
};
|
};
|
||||||
// Tussenruimte naast elkaar: 2x stijldikte i.p.v. 1x: de OSB-uitsparing
|
// Tussenruimte naast elkaar: 2x stijldikte i.p.v. 1x: de OSB-uitsparing
|
||||||
@@ -337,23 +356,22 @@ window.WallModel = (() => {
|
|||||||
if (cur.x > maxX) cur.x = maxX;
|
if (cur.x > maxX) cur.x = maxX;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fase 2: hoogte (h) en verticale positie (y), per opening geklemd tegen
|
// Fase 2: hoogte (h links, hR rechts) en verticale positie (y), per
|
||||||
// de daklijn zoals die loopt op de EIGEN (nu definitieve) x-positie van
|
// opening geklemd tegen de daklijn zoals die loopt op de EIGEN (nu
|
||||||
// die opening — het punt waar een verticale lijn op die plek de
|
// definitieve) x-positie van die opening — per kant: de linkerhoogte
|
||||||
// (eventueel hellende) horizontale topbalk zou snijden. Bij een
|
// tegen de daklijn boven de linkerrand, de rechterhoogte tegen die
|
||||||
// opening die zelf een breedte heeft, wordt het laagste van de twee
|
// boven de rechterrand. Zo kan een schuine bovenzijde het dak volgen.
|
||||||
// uiteinden aangehouden (de daklijn is lineair of heeft een nok als
|
// Daartussen blijft de bovenkant onder het dak: de daklijn is lineair
|
||||||
// hoogste punt, dus dat is voldoende).
|
// of heeft een nok als hoogste punt (bol), dus de rechte lijn tussen
|
||||||
const localRoofHeightAt = roofAt;
|
// twee punten eronder blijft eronder.
|
||||||
|
|
||||||
resolved.forEach(o => {
|
resolved.forEach(o => {
|
||||||
const localCeiling = Math.min(localRoofHeightAt(o.x), localRoofHeightAt(o.x + o.w));
|
const ceilL = roofAt(o.x), ceilR = roofAt(o.x + o.w);
|
||||||
const maxAllowedOpH = Math.max(400, localCeiling - (plateThick * 2));
|
const hL = Math.min(Math.max(400, o.rawH), Math.max(400, ceilL - plateThick * 2));
|
||||||
const h = Math.min(Math.max(400, o.rawH), maxAllowedOpH);
|
const hR = Math.min(Math.max(400, o.rawHR), Math.max(400, ceilR - plateThick * 2));
|
||||||
const maxOpY = Math.max(0, localCeiling - h - plateThick);
|
const maxOpY = Math.max(0, Math.min(ceilL - hL, ceilR - hR) - plateThick);
|
||||||
const y = Math.max(0, Math.min(o.rawY, maxOpY));
|
o.y = Math.max(0, Math.min(o.rawY, maxOpY));
|
||||||
o.h = h;
|
o.h = hL;
|
||||||
o.y = y;
|
o.hR = hR;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
// Detecteer of het na dit alles nog steeds niet past (te veel/te brede
|
||||||
@@ -457,10 +475,16 @@ window.WallModel = (() => {
|
|||||||
// kreupel boven/onder een opening, binnen de openingbreedte
|
// kreupel boven/onder een opening, binnen de openingbreedte
|
||||||
const sw = CONFIG.studWidth;
|
const sw = CONFIG.studWidth;
|
||||||
const ops = CONFIG.openings; // al gesorteerd op x door resolveOpenings
|
const ops = CONFIG.openings; // al gesorteerd op x door resolveOpenings
|
||||||
const bands = ops.map(op => ({
|
// Bovendorpel volgt een schuine bovenzijde (loodrechte dikte sw):
|
||||||
op, x0: startX + op.x, x1: startX + op.x + op.w,
|
// topAt(x) = bovenkant dorpel, y1 = het hoogste punt daarvan.
|
||||||
y0: op.y > CONFIG.plateThick ? op.y - sw : 0, y1: op.y + op.h + sw
|
const bands = ops.map(op => {
|
||||||
}));
|
const x0 = startX + op.x, s = (op.hR - op.h) / op.w, vo = sw * Math.sqrt(1 + s * s);
|
||||||
|
return {
|
||||||
|
op, x0, x1: x0 + op.w,
|
||||||
|
y0: op.y > CONFIG.plateThick ? op.y - sw : 0, y1: op.y + Math.max(op.h, op.hR) + vo,
|
||||||
|
topAt: (x) => op.y + op.h + (x - x0) * s + vo
|
||||||
|
};
|
||||||
|
});
|
||||||
const cands = [];
|
const cands = [];
|
||||||
cornerStuds.forEach(x => cands.push({ x, kind: 'hoek' }));
|
cornerStuds.forEach(x => cands.push({ x, kind: 'hoek' }));
|
||||||
ops.forEach(op => {
|
ops.forEach(op => {
|
||||||
@@ -497,8 +521,11 @@ window.WallModel = (() => {
|
|||||||
cands.forEach(c => {
|
cands.forEach(c => {
|
||||||
const x = Math.round(c.x);
|
const x = Math.round(c.x);
|
||||||
const hit = bands.filter(bd => Math.min(bd.x1, x + sw / 2) - Math.max(bd.x0, x - sw / 2) > 0.5).sort((p, q) => p.y0 - q.y0);
|
const hit = bands.filter(bd => Math.min(bd.x1, x + sw / 2) - Math.max(bd.x0, x - sw / 2) > 0.5).sort((p, q) => p.y0 - q.y0);
|
||||||
let cur = CONFIG.plateThick, below = null;
|
// cur = onderkant van het volgende stuk [links, rechts]: schuin als
|
||||||
const push = (y0, y1, above) => {
|
// het op een schuine bovendorpel staat (dan ook y0l/y0r; y0 = laagste).
|
||||||
|
let cur = [CONFIG.plateThick, CONFIG.plateThick], below = null;
|
||||||
|
const curMax = () => Math.max(cur[0], cur[1]);
|
||||||
|
const push = (y1, above) => {
|
||||||
if (c.kind === 'kreupel') {
|
if (c.kind === 'kreupel') {
|
||||||
// Alleen direct boven of onder de eigen opening; onder de opening
|
// Alleen direct boven of onder de eigen opening; onder de opening
|
||||||
// niet als hij op een andere opening uitkomt (dat stuk vult de
|
// niet als hij op een andere opening uitkomt (dat stuk vult de
|
||||||
@@ -506,13 +533,16 @@ window.WallModel = (() => {
|
|||||||
const onTop = below && below.op === c.owner, underneath = above && above.op === c.owner;
|
const onTop = below && below.op === c.owner, underneath = above && above.op === c.owner;
|
||||||
if (!onTop && !(underneath && !below)) return;
|
if (!onTop && !(underneath && !below)) return;
|
||||||
}
|
}
|
||||||
segs.push({ x, y0, y1, kind: c.kind });
|
const sg = { x, y0: Math.min(cur[0], cur[1]), y1, kind: c.kind };
|
||||||
|
if (Math.abs(cur[0] - cur[1]) > 0.05) { sg.y0l = cur[0]; sg.y0r = cur[1]; }
|
||||||
|
segs.push(sg);
|
||||||
};
|
};
|
||||||
hit.forEach(bd => {
|
hit.forEach(bd => {
|
||||||
if (bd.y0 > cur + 1) push(cur, bd.y0, bd);
|
if (bd.y0 > curMax() + 1) push(bd.y0, bd);
|
||||||
if (bd.y1 > cur) { cur = bd.y1; below = bd; }
|
const t = [bd.topAt(x - sw / 2), bd.topAt(x + sw / 2)];
|
||||||
|
if (Math.max(t[0], t[1]) > curMax()) { cur = t; below = bd; }
|
||||||
});
|
});
|
||||||
if (roofMin(x) > cur + 1) push(cur, null, null);
|
if (roofMin(x) > curMax() + 1) push(null, null);
|
||||||
});
|
});
|
||||||
// Dubbelingen (bijv. koningsstijl op de hoekstijl) weg: hoogste prioriteit wint.
|
// Dubbelingen (bijv. koningsstijl op de hoekstijl) weg: hoogste prioriteit wint.
|
||||||
const studs = [];
|
const studs = [];
|
||||||
@@ -536,19 +566,30 @@ window.WallModel = (() => {
|
|||||||
// bij een sterk hellende wand met een opening dicht bij de lage kant kan
|
// bij een sterk hellende wand met een opening dicht bij de lage kant kan
|
||||||
// de marge anders lokaal boven het dak uitkomen (kapotte uitsparing).
|
// de marge anders lokaal boven het dak uitkomen (kapotte uitsparing).
|
||||||
// Gebruikt door build3D (fill) én de 2D-tekening (naad-/baanlijnen).
|
// Gebruikt door build3D (fill) én de 2D-tekening (naad-/baanlijnen).
|
||||||
|
// Schuine bovenzijde: hy1 = bovenkant links (bij hx0), hy1r = rechts (bij
|
||||||
|
// hx1), langs de bovenkant van de schuine bovendorpel; per kant geklemd
|
||||||
|
// (de daklijn is bol, dus de lijn ertussen blijft eronder).
|
||||||
function sheathingHoles(CONFIG) {
|
function sheathingHoles(CONFIG) {
|
||||||
const halfW = CONFIG.wallWidth / 2;
|
const halfW = CONFIG.wallWidth / 2;
|
||||||
const topYAt = topLine(CONFIG).at;
|
const topYAt = topLine(CONFIG).at;
|
||||||
const roofSafetyMargin = 10;
|
const roofSafetyMargin = 10;
|
||||||
|
const sw = CONFIG.studWidth;
|
||||||
return CONFIG.openings.map(op => {
|
return CONFIG.openings.map(op => {
|
||||||
const hx0 = -halfW + op.x - CONFIG.studWidth;
|
const hx0 = -halfW + op.x - sw;
|
||||||
const hx1 = -halfW + op.x + op.w + CONFIG.studWidth;
|
const hx1 = -halfW + op.x + op.w + sw;
|
||||||
let hy1 = op.y + op.h + CONFIG.studWidth;
|
const s = (op.hR - op.h) / op.w, vo = sw * Math.sqrt(1 + s * s);
|
||||||
const localRoofCeiling = Math.min(topYAt(hx0), topYAt(hx1)) - roofSafetyMargin;
|
const clampTop = (y, x) => {
|
||||||
if (hy1 > localRoofCeiling) hy1 = Math.max(op.y + roofSafetyMargin, localRoofCeiling);
|
const ceil = topYAt(x) - roofSafetyMargin;
|
||||||
return { hx0, hx1, hy0: op.y, hy1 };
|
return y > ceil ? Math.max(op.y + roofSafetyMargin, ceil) : y;
|
||||||
|
};
|
||||||
|
const hy1 = clampTop(op.y + op.h + vo - s * sw, hx0);
|
||||||
|
const hy1r = clampTop(op.y + op.hR + vo + s * sw, hx1);
|
||||||
|
return { hx0, hx1, hy0: op.y, hy1, hy1r };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const holeTopR = (h) => h.hy1r !== undefined ? h.hy1r : h.hy1;
|
||||||
|
const holeTopAt = (h, x) => h.hy1 + (x - h.hx0) / (h.hx1 - h.hx0) * (holeTopR(h) - h.hy1);
|
||||||
|
const holeRect = (h) => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1, y1r: holeTopR(h) });
|
||||||
|
|
||||||
// Stukken van een horizontale lijn op hoogte y (mm) die OP de beplating
|
// Stukken van een horizontale lijn op hoogte y (mm) die OP de beplating
|
||||||
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
|
// liggen: binnen de wand-omtrek (incl. schuine top) en buiten de
|
||||||
@@ -559,12 +600,15 @@ window.WallModel = (() => {
|
|||||||
const range = topLine(CONFIG).rangeAbove(y);
|
const range = topLine(CONFIG).rangeAbove(y);
|
||||||
if (!range) return [];
|
if (!range) return [];
|
||||||
let segments = [range];
|
let segments = [range];
|
||||||
sheathingHoles(CONFIG).concat(extraHoles || []).filter(h => y > h.hy0 && y < h.hy1).forEach(h => {
|
sheathingHoles(CONFIG).concat(extraHoles || []).forEach(h => {
|
||||||
|
const span = slopedSpanAtY(h.hx0, h.hx1, h.hy0, h.hy1, holeTopR(h), y);
|
||||||
|
if (!span) return;
|
||||||
|
const [hx0, hx1] = span;
|
||||||
segments = segments.flatMap(([a, b]) => {
|
segments = segments.flatMap(([a, b]) => {
|
||||||
if (h.hx1 <= a || h.hx0 >= b) return [[a, b]];
|
if (hx1 <= a || hx0 >= b) return [[a, b]];
|
||||||
const out = [];
|
const out = [];
|
||||||
if (h.hx0 > a) out.push([a, h.hx0]);
|
if (hx0 > a) out.push([a, hx0]);
|
||||||
if (h.hx1 < b) out.push([h.hx1, b]);
|
if (hx1 < b) out.push([hx1, b]);
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -625,11 +669,13 @@ window.WallModel = (() => {
|
|||||||
const yTop = Math.min(topYAt(x - w / 2), topYAt(x + w / 2));
|
const yTop = Math.min(topYAt(x - w / 2), topYAt(x + w / 2));
|
||||||
let segs = [[y0, yTop]];
|
let segs = [[y0, yTop]];
|
||||||
holes.filter(h => x + w / 2 > h.hx0 && x - w / 2 < h.hx1).forEach(h => {
|
holes.filter(h => x + w / 2 > h.hx0 && x - w / 2 < h.hx1).forEach(h => {
|
||||||
|
// Schuine bovenkant: de hoogste waarde over de latbreedte.
|
||||||
|
const hy1 = Math.max(holeTopAt(h, Math.max(h.hx0, x - w / 2)), holeTopAt(h, Math.min(h.hx1, x + w / 2)));
|
||||||
segs = segs.flatMap(([a, b]) => {
|
segs = segs.flatMap(([a, b]) => {
|
||||||
if (h.hy1 <= a || h.hy0 >= b) return [[a, b]];
|
if (hy1 <= a || h.hy0 >= b) return [[a, b]];
|
||||||
const out = [];
|
const out = [];
|
||||||
if (h.hy0 > a) out.push([a, h.hy0]);
|
if (h.hy0 > a) out.push([a, h.hy0]);
|
||||||
if (h.hy1 < b) out.push([h.hy1, b]);
|
if (hy1 < b) out.push([hy1, b]);
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -815,7 +861,8 @@ window.WallModel = (() => {
|
|||||||
xMin: startXM + op.x * toM,
|
xMin: startXM + op.x * toM,
|
||||||
xMax: startXM + (op.x + op.w) * toM,
|
xMax: startXM + (op.x + op.w) * toM,
|
||||||
yMin: op.y * toM,
|
yMin: op.y * toM,
|
||||||
yMax: (op.y + op.h) * toM,
|
yMax: (op.y + op.h) * toM, // bovenkant links
|
||||||
|
yMaxR: (op.y + op.hR) * toM, // bovenkant rechts (schuine bovenzijde)
|
||||||
wM: op.w * toM
|
wM: op.w * toM
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -864,21 +911,24 @@ window.WallModel = (() => {
|
|||||||
const xM = st.x * toM;
|
const xM = st.x * toM;
|
||||||
const xLeft = xM - (sWidthM / 2);
|
const xLeft = xM - (sWidthM / 2);
|
||||||
const xRight = xM + (sWidthM / 2);
|
const xRight = xM + (sWidthM / 2);
|
||||||
const y0 = st.y0 * toM;
|
// Onderkant schuin als het stuk op een schuine bovendorpel staat.
|
||||||
|
const y0l = (st.y0l !== undefined ? st.y0l : st.y0) * toM, y0r = (st.y0r !== undefined ? st.y0r : st.y0) * toM;
|
||||||
const pts = st.y1 === null
|
const pts = st.y1 === null
|
||||||
? [{ x: xLeft, y: y0 }, { x: xRight, y: y0 }, ...underPts(xLeft, xRight).reverse()]
|
? [{ x: xLeft, y: y0l }, { x: xRight, y: y0r }, ...underPts(xLeft, xRight).reverse()]
|
||||||
: [{ x: xLeft, y: y0 }, { x: xRight, y: y0 }, { x: xRight, y: st.y1 * toM }, { x: xLeft, y: st.y1 * toM }];
|
: [{ x: xLeft, y: y0l }, { x: xRight, y: y0r }, { x: xRight, y: st.y1 * toM }, { x: xLeft, y: st.y1 * toM }];
|
||||||
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2));
|
||||||
});
|
});
|
||||||
|
|
||||||
// D. DORPELS, per opening
|
// D. DORPELS, per opening
|
||||||
opsM.forEach(op => {
|
opsM.forEach(op => {
|
||||||
// Bovendorpel / linteel (horizontale balk)
|
// Bovendorpel / linteel (liggende balk, volgt een schuine bovenzijde;
|
||||||
|
// loodrechte dikte = stijldikte)
|
||||||
|
const s = (op.yMaxR - op.yMax) / op.wM, vo = sWidthM * Math.sqrt(1 + s * s);
|
||||||
const opTopShape = new THREE.Shape();
|
const opTopShape = new THREE.Shape();
|
||||||
opTopShape.moveTo(op.xMin, op.yMax);
|
opTopShape.moveTo(op.xMin, op.yMax);
|
||||||
opTopShape.lineTo(op.xMax, op.yMax);
|
opTopShape.lineTo(op.xMax, op.yMaxR);
|
||||||
opTopShape.lineTo(op.xMax, op.yMax + sWidthM);
|
opTopShape.lineTo(op.xMax, op.yMaxR + vo);
|
||||||
opTopShape.lineTo(op.xMin, op.yMax + sWidthM);
|
opTopShape.lineTo(op.xMin, op.yMax + vo);
|
||||||
opTopShape.closePath();
|
opTopShape.closePath();
|
||||||
wallGroup3D.add(createExtrudedBeam(opTopShape, sDepthM, -sDepthM / 2));
|
wallGroup3D.add(createExtrudedBeam(opTopShape, sDepthM, -sDepthM / 2));
|
||||||
horizBeamCount++;
|
horizBeamCount++;
|
||||||
@@ -949,7 +999,7 @@ window.WallModel = (() => {
|
|||||||
// Hoekdoorloop per buitenlaag, in m (zie cornerExt).
|
// Hoekdoorloop per buitenlaag, in m (zie cornerExt).
|
||||||
const extM = (depthMM) => { const e = cornerExt(skinExt, depthMM); return { left: e.left * toM, right: e.right * toM }; };
|
const extM = (depthMM) => { const e = cornerExt(skinExt, depthMM); return { left: e.left * toM, right: e.right * toM }; };
|
||||||
const holeExtents = sheathingHoles(CONFIG).map(hole => ({
|
const holeExtents = sheathingHoles(CONFIG).map(hole => ({
|
||||||
hx0: hole.hx0 * toM, hx1: hole.hx1 * toM, hy0: hole.hy0 * toM, hy1: hole.hy1 * toM
|
hx0: hole.hx0 * toM, hx1: hole.hx1 * toM, hy0: hole.hy0 * toM, hy1: hole.hy1 * toM, hy1r: hole.hy1r * toM
|
||||||
}));
|
}));
|
||||||
// Binnenhoek-doorloop (negatieve skinExt): dat stuk zit binnen de wand
|
// Binnenhoek-doorloop (negatieve skinExt): dat stuk zit binnen de wand
|
||||||
// en het dak van de buur-unit; daar geen buitenlagen, tot opts.trimTop
|
// en het dak van de buur-unit; daar geen buitenlagen, tot opts.trimTop
|
||||||
@@ -1008,13 +1058,13 @@ window.WallModel = (() => {
|
|||||||
const xL = -halfW - ext.left, xR = halfW + ext.right;
|
const xL = -halfW - ext.left, xR = halfW + ext.right;
|
||||||
const capL = ext.left > 0 ? xL + tAll : xL, capR = ext.right > 0 ? xR - tAll : xR;
|
const capL = ext.left > 0 ? xL + tAll : xL, capR = ext.right > 0 ? xR - tAll : xR;
|
||||||
const topClamped = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x)));
|
const topClamped = (x) => tlOut.at(Math.max(-halfW, Math.min(halfW, x)));
|
||||||
const rects = outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 }));
|
const rects = outerHoles.map(holeRect);
|
||||||
const shapesIn = (a, b) => {
|
const shapesIn = (a, b) => {
|
||||||
if (b - a <= edgeEps) return [];
|
if (b - a <= edgeEps) return [];
|
||||||
const capped = yCap === Infinity;
|
const capped = yCap === Infinity;
|
||||||
const pieces = columnMinusRects(a, b, y0, capped ? topClamped : (x) => Math.min(yCap, topClamped(x)), rects,
|
const pieces = columnMinusRects(a, b, y0, capped ? topClamped : (x) => Math.min(yCap, topClamped(x)), rects,
|
||||||
capped ? { extraXs: tlOut.kinks } : { yCap, extraXs: tlOut.kinks.concat(tlOut.crossings(yCap)) });
|
capped ? { extraXs: tlOut.kinks } : { yCap, extraXs: tlOut.kinks.concat(tlOut.crossings(yCap)) });
|
||||||
return (pieces || []).map(piece => {
|
return pieces.map(piece => {
|
||||||
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
|
const shape = new THREE.Shape(piece.outer.map(q => new THREE.Vector2(q.x, q.y)));
|
||||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(q => new THREE.Vector2(q.x, q.y)))));
|
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(q => new THREE.Vector2(q.x, q.y)))));
|
||||||
return shape;
|
return shape;
|
||||||
@@ -1078,42 +1128,13 @@ window.WallModel = (() => {
|
|||||||
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
|
// over een plaatnaad wordt een inkeping in de plaatrand, zodat er
|
||||||
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
|
// geen reepje plaat (met randlijnen) door de opening blijft lopen.
|
||||||
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
|
const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt,
|
||||||
HX.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })),
|
HX.map(holeRect),
|
||||||
{ yCap: range ? yCap : undefined, extraXs: range ? TL.kinks.concat(TL.crossings(yCap)) : TL.kinks });
|
{ yCap: range ? yCap : undefined, extraXs: range ? TL.kinks.concat(TL.crossings(yCap)) : TL.kinks });
|
||||||
let boardShapes;
|
const boardShapes = pieces.map(piece => {
|
||||||
if (pieces) {
|
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
||||||
boardShapes = pieces.map(piece => {
|
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
||||||
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
return shape;
|
||||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
});
|
||||||
return shape;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Terugval (uitsparing tot in de schuine bovenrand): oude
|
|
||||||
// gat-methode, net binnen de plaatrand afgekapt.
|
|
||||||
const boardShape = new THREE.Shape();
|
|
||||||
boardShape.moveTo(ex0, yB);
|
|
||||||
boardShape.lineTo(ex1, yB);
|
|
||||||
boardShape.lineTo(ex1, skinTopAt(ex1));
|
|
||||||
TL.kinks.filter(x => x > ex0 && x < ex1).forEach(x => boardShape.lineTo(x, skinTopAt(x)));
|
|
||||||
boardShape.lineTo(ex0, skinTopAt(ex0));
|
|
||||||
boardShape.closePath();
|
|
||||||
holeExtents.forEach(h => {
|
|
||||||
const chx0 = Math.max(ex0 + edgeEps, h.hx0);
|
|
||||||
const chx1 = Math.min(ex1 - edgeEps, h.hx1);
|
|
||||||
if (chx1 > chx0) {
|
|
||||||
const holePath = new THREE.Path();
|
|
||||||
const chy0 = Math.max(yB + edgeEps, h.hy0), chy1 = Math.min(yCap - edgeEps, h.hy1);
|
|
||||||
if (chy1 <= chy0) return;
|
|
||||||
holePath.moveTo(chx0, chy0);
|
|
||||||
holePath.lineTo(chx0, chy1);
|
|
||||||
holePath.lineTo(chx1, chy1);
|
|
||||||
holePath.lineTo(chx1, chy0);
|
|
||||||
holePath.closePath();
|
|
||||||
boardShape.holes.push(holePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
boardShapes = [boardShape];
|
|
||||||
}
|
|
||||||
if (!boardShapes.length) continue;
|
if (!boardShapes.length) continue;
|
||||||
|
|
||||||
const boardExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
const boardExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
||||||
@@ -1236,7 +1257,6 @@ window.WallModel = (() => {
|
|||||||
// transparant zijn).
|
// transparant zijn).
|
||||||
const doekZStepM = 0.0002;
|
const doekZStepM = 0.0002;
|
||||||
const DOEK_OUT_M = 0.001; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
const DOEK_OUT_M = 0.001; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte
|
||||||
const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tlOut.pts.slice().reverse()];
|
|
||||||
const maxRoofY = tlOut.maxY;
|
const maxRoofY = tlOut.maxY;
|
||||||
|
|
||||||
let rowY0 = skinFrom * toM;
|
let rowY0 = skinFrom * toM;
|
||||||
@@ -1251,41 +1271,13 @@ window.WallModel = (() => {
|
|||||||
const [rx0, rx1] = tlOut.rangeAbove(rowY0) || [0, 0];
|
const [rx0, rx1] = tlOut.rangeAbove(rowY0) || [0, 0];
|
||||||
const extraXs = tlOut.crossings(rowY1).concat(tlOut.kinks);
|
const extraXs = tlOut.crossings(rowY1).concat(tlOut.kinks);
|
||||||
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
|
const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)),
|
||||||
outerHoles.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : [];
|
outerHoles.map(holeRect), { yCap: rowY1, extraXs }) : [];
|
||||||
|
|
||||||
let rowShapes;
|
const rowShapes = pieces.map(piece => {
|
||||||
if (pieces) {
|
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
||||||
rowShapes = pieces.map(piece => {
|
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
||||||
const shape = new THREE.Shape(piece.outer.map(p => new THREE.Vector2(p.x, p.y)));
|
return shape;
|
||||||
piece.holes.forEach(hole => shape.holes.push(new THREE.Path(hole.map(p => new THREE.Vector2(p.x, p.y)))));
|
});
|
||||||
return shape;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Terugval (uitsparing tot in de schuine bovenrand): oude methode.
|
|
||||||
let rowPts = clipPolygonYLE(wallOutlinePts, rowY1);
|
|
||||||
rowPts = clipPolygonYGE(rowPts, rowY0);
|
|
||||||
rowShapes = [];
|
|
||||||
if (rowPts.length >= 3) {
|
|
||||||
const rowShape = new THREE.Shape();
|
|
||||||
rowShape.moveTo(rowPts[0].x, rowPts[0].y);
|
|
||||||
for (let k = 1; k < rowPts.length; k++) rowShape.lineTo(rowPts[k].x, rowPts[k].y);
|
|
||||||
rowShape.closePath();
|
|
||||||
holeExtents.forEach(h => {
|
|
||||||
const chy0 = Math.max(rowY0 + edgeEps, h.hy0);
|
|
||||||
const chy1 = Math.min(rowY1 - edgeEps, h.hy1);
|
|
||||||
if (chy1 > chy0) {
|
|
||||||
const holePath = new THREE.Path();
|
|
||||||
holePath.moveTo(h.hx0, chy0);
|
|
||||||
holePath.lineTo(h.hx0, chy1);
|
|
||||||
holePath.lineTo(h.hx1, chy1);
|
|
||||||
holePath.lineTo(h.hx1, chy0);
|
|
||||||
holePath.closePath();
|
|
||||||
rowShape.holes.push(holePath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
rowShapes.push(rowShape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rowShapes.length) {
|
if (rowShapes.length) {
|
||||||
const rowExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
const rowExtrudeSettings = { steps: 1, depth: sThickM, bevelEnabled: false };
|
||||||
const rowGeo = shiftUV(new THREE.ExtrudeGeometry(rowShapes, rowExtrudeSettings));
|
const rowGeo = shiftUV(new THREE.ExtrudeGeometry(rowShapes, rowExtrudeSettings));
|
||||||
@@ -1388,9 +1380,7 @@ window.WallModel = (() => {
|
|||||||
const envelopeAreaM2 = polygonArea(envelopePoints);
|
const envelopeAreaM2 = polygonArea(envelopePoints);
|
||||||
const envelopeVolumeM3 = envelopeAreaM2 * sDepthM;
|
const envelopeVolumeM3 = envelopeAreaM2 * sDepthM;
|
||||||
|
|
||||||
const openingsVolumeM3 = CONFIG.openings.reduce((sum, op) => {
|
const openingsVolumeM3 = CONFIG.openings.reduce((sum, op) => sum + openingArea(op) * toM * toM * sDepthM, 0);
|
||||||
return sum + (op.w * toM) * (op.h * toM) * sDepthM;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
let voidVolumeM3 = envelopeVolumeM3 - stats.woodVolumeM3 - openingsVolumeM3;
|
let voidVolumeM3 = envelopeVolumeM3 - stats.woodVolumeM3 - openingsVolumeM3;
|
||||||
if (voidVolumeM3 < 0) voidVolumeM3 = 0;
|
if (voidVolumeM3 < 0) voidVolumeM3 = 0;
|
||||||
@@ -1474,11 +1464,14 @@ window.WallModel = (() => {
|
|||||||
const x = st.x, xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
const x = st.x, xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2;
|
||||||
const yBottom = st.y0;
|
const yBottom = st.y0;
|
||||||
const kind = st.kind === 'kreupel' ? 'kreupele stijl' : 'verticale balk';
|
const kind = st.kind === 'kreupel' ? 'kreupele stijl' : 'verticale balk';
|
||||||
|
// Schuine onderkant: stuk op een schuine bovendorpel.
|
||||||
|
const yBottomLeft = st.y0l !== undefined ? st.y0l : yBottom, yBottomRight = st.y0r !== undefined ? st.y0r : yBottom;
|
||||||
|
const botCut = cutDeg(yBottomRight - yBottomLeft, xRight - xLeft);
|
||||||
if (st.y1 !== null) {
|
if (st.y1 !== null) {
|
||||||
pieces.push({
|
pieces.push({
|
||||||
length: st.y1 - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
length: st.y1 - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
||||||
y: (yBottom + st.y1) / 2,
|
y: (yBottom + st.y1) / 2,
|
||||||
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft: st.y1, yTopRight: st.y1, cutAngles: [0, 0]
|
yBottomLeft, yBottomRight, yTopLeft: st.y1, yTopRight: st.y1, cutAngles: [botCut, 0]
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1486,16 +1479,24 @@ window.WallModel = (() => {
|
|||||||
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y;
|
||||||
pieces.push({
|
pieces.push({
|
||||||
length: Math.max(...topPts.map(p => p.y)) - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
length: Math.max(...topPts.map(p => p.y)) - yBottom, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind, x,
|
||||||
y: (yBottom + yBottom + yTopLeft + yTopRight) / 4,
|
y: (yBottomLeft + yBottomRight + yTopLeft + yTopRight) / 4,
|
||||||
yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight,
|
yBottomLeft, yBottomRight, yTopLeft, yTopRight,
|
||||||
outline: [{ x: xLeft, y: yBottom }, { x: xRight, y: yBottom }, ...topPts.slice().reverse()],
|
outline: [{ x: xLeft, y: yBottomLeft }, { x: xRight, y: yBottomRight }, ...topPts.slice().reverse()],
|
||||||
cutAngles: [0].concat(topPts.slice(1).map((q, k) => cutDeg(q.y - topPts[k].y, q.x - topPts[k].x)))
|
cutAngles: [botCut].concat(topPts.slice(1).map((q, k) => cutDeg(q.y - topPts[k].y, q.x - topPts[k].x)))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
const opXWorld = startX + op.x;
|
const opXWorld = startX + op.x;
|
||||||
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + op.h + CONFIG.studWidth / 2, cutAngles: [0, 0] });
|
// Schuine bovenzijde: dorpel langs de helling (loodrechte sneden,
|
||||||
|
// zoals de bovenregel), met de werkelijke omtrek voor de tekening.
|
||||||
|
const dh = op.hR - op.h;
|
||||||
|
const header = { length: Math.hypot(op.w, dh), width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'bovendorpel', x: opXWorld + op.w / 2, y: op.y + (op.h + op.hR) / 2 + CONFIG.studWidth / 2, cutAngles: [cutDeg(dh, op.w), cutDeg(dh, op.w)] };
|
||||||
|
if (Math.abs(dh) > 0.5) {
|
||||||
|
const vo = CONFIG.studWidth * Math.hypot(op.w, dh) / op.w;
|
||||||
|
header.outline = [{ x: opXWorld, y: op.y + op.h }, { x: opXWorld + op.w, y: op.y + op.hR }, { x: opXWorld + op.w, y: op.y + op.hR + vo }, { x: opXWorld, y: op.y + op.h + vo }];
|
||||||
|
}
|
||||||
|
pieces.push(header);
|
||||||
if (op.y > CONFIG.plateThick) {
|
if (op.y > CONFIG.plateThick) {
|
||||||
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'onderdorpel', x: opXWorld + op.w / 2, y: op.y - CONFIG.studWidth / 2, cutAngles: [0, 0] });
|
pieces.push({ length: op.w, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'onderdorpel', x: opXWorld + op.w / 2, y: op.y - CONFIG.studWidth / 2, cutAngles: [0, 0] });
|
||||||
}
|
}
|
||||||
@@ -1573,6 +1574,9 @@ window.WallModel = (() => {
|
|||||||
if (typeof o.w !== 'number' || typeof o.h !== 'number' || typeof o.y !== 'number' || typeof o.offset !== 'number') {
|
if (typeof o.w !== 'number' || typeof o.h !== 'number' || typeof o.y !== 'number' || typeof o.offset !== 'number') {
|
||||||
throw new Error(`opening #${i + 1}: w/h/y/offset moeten getallen zijn`);
|
throw new Error(`opening #${i + 1}: w/h/y/offset moeten getallen zijn`);
|
||||||
}
|
}
|
||||||
|
if (o.hR !== undefined && (typeof o.hR !== 'number' || !isFinite(o.hR))) {
|
||||||
|
throw new Error(`opening #${i + 1}: hR (hoogte rechts) moet een getal zijn`);
|
||||||
|
}
|
||||||
if (o.type !== undefined && !OPENING_TYPES[o.type]) {
|
if (o.type !== undefined && !OPENING_TYPES[o.type]) {
|
||||||
throw new Error(`opening #${i + 1}: type moet ${Object.keys(OPENING_TYPES).join('/')} zijn (was: ${o.type})`);
|
throw new Error(`opening #${i + 1}: type moet ${Object.keys(OPENING_TYPES).join('/')} zijn (was: ${o.type})`);
|
||||||
}
|
}
|
||||||
@@ -1584,7 +1588,7 @@ window.WallModel = (() => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
STANDARD_BEAM, STOCK_LENGTHS_MM, CORNER_STUDS, EXTERIOR_MATERIALS, OPENING_TYPES, BATTEN, CLADDING_DIRECTIONS, CLADDING_THICK_MM, claddingOffset, cornerExt, TRASRAAM_DEFAULT_MM, STONE, STONE_COLORS, TRASRAAM_STONES, isStoneFinish, stoneThumbUrl, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
|
||||||
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
|
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines, openingTopAt, openingTopMax, openingArea, openingSpanAtY,
|
||||||
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
|
||||||
createMaterials, build3D, computeSpecs
|
createMaterials, build3D, computeSpecs
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-4
@@ -154,16 +154,17 @@ window.WallPdf = (() => {
|
|||||||
doc.setLineWidth(0.2);
|
doc.setLineWidth(0.2);
|
||||||
CONFIG.openings.forEach(op => {
|
CONFIG.openings.forEach(op => {
|
||||||
const opXWorld = -wallWidth / 2 + op.x;
|
const opXWorld = -wallWidth / 2 + op.x;
|
||||||
const x0 = px(opXWorld), y0 = py(op.y + op.h), x1 = px(opXWorld + op.w), y1 = py(op.y);
|
// Schuine bovenzijde: y0 = bovenkant links, y0r = rechts.
|
||||||
doc.rect(x0, y0, x1 - x0, y1 - y0, 'S');
|
const x0 = px(opXWorld), y0 = py(op.y + op.h), y0r = py(op.y + op.hR), x1 = px(opXWorld + op.w), y1 = py(op.y);
|
||||||
|
[[x0, y1, x1, y1], [x1, y1, x1, y0r], [x1, y0r, x0, y0], [x0, y0, x0, y1]].forEach(l => doc.line(...l));
|
||||||
doc.line(x0, y0, x1, y1);
|
doc.line(x0, y0, x1, y1);
|
||||||
doc.line(x0, y1, x1, y0);
|
doc.line(x0, y1, x1, y0r);
|
||||||
doc.setTextColor(...BLACK);
|
doc.setTextColor(...BLACK);
|
||||||
doc.setFontSize(6);
|
doc.setFontSize(6);
|
||||||
// Dagmaat-label BINNEN de opening zetten (net onder de bovenkant),
|
// Dagmaat-label BINNEN de opening zetten (net onder de bovenkant),
|
||||||
// i.p.v. erboven — daar overlapte het met de maatketen/omtrek
|
// i.p.v. erboven — daar overlapte het met de maatketen/omtrek
|
||||||
// erboven.
|
// erboven.
|
||||||
doc.text(`${op.w}x${op.h}`, (x0 + x1) / 2, y0 + 5, { align: 'center' });
|
doc.text(op.hR !== op.h ? `${op.w}x${op.h}/${op.hR}` : `${op.w}x${op.h}`, (x0 + x1) / 2, Math.max(y0, y0r) + 5, { align: 'center' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const drawDimLine = (x1, y1, x2, y2) => PdfSheet.dimLine(doc, x1, y1, x2, y2);
|
const drawDimLine = (x1, y1, x2, y2) => PdfSheet.dimLine(doc, x1, y1, x2, y2);
|
||||||
|
|||||||
+107
-33
@@ -280,7 +280,9 @@ function mountWallEditor(container, callbacks) {
|
|||||||
let wallGroup3D = null;
|
let wallGroup3D = null;
|
||||||
let lastBaseHeight = 2400;
|
let lastBaseHeight = 2400;
|
||||||
|
|
||||||
// Elke opening: { id, w, h, y, anchor: 'left'|'middle'|'right', offset }
|
// Elke opening: { id, w, h, hR?, y, anchor: 'left'|'middle'|'right', offset }
|
||||||
|
// hR = hoogte rechts (schuine bovenzijde); zonder hR is de bovenkant
|
||||||
|
// recht en is h de hoogte. De onderkant is altijd waterpas.
|
||||||
// 'offset' betekent afhankelijk van anchor:
|
// 'offset' betekent afhankelijk van anchor:
|
||||||
// left -> afstand vanaf linker wandrand tot linkerkant opening
|
// left -> afstand vanaf linker wandrand tot linkerkant opening
|
||||||
// right -> afstand vanaf rechter wandrand tot rechterkant opening
|
// right -> afstand vanaf rechter wandrand tot rechterkant opening
|
||||||
@@ -293,11 +295,14 @@ function mountWallEditor(container, callbacks) {
|
|||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
// Interactieve selectie/verslepen van openingen in het vooraanzicht
|
// Interactieve selectie/verslepen van openingen in het vooraanzicht
|
||||||
// (nu het primaire aanzicht). Snap op 50mm, net als de stapgrootte van
|
// (nu het primaire aanzicht). Snap op 50mm, net als de stapgrootte van
|
||||||
// de invoervelden. N/Z-handles wijzigen alleen de hoogte, O/W-handles
|
// de invoervelden. 9 sleeppunten: 4 hoeken, 4 zijden en het midden
|
||||||
// alleen de breedte — precies zoals bij de meeste 2D-tekentools.
|
// (verplaatsen). N/Z wijzigen de hoogte, O/W de breedte. De bovenste
|
||||||
|
// hoeken (NW/NO) verslepen ook alleen hun eigen kant in de hoogte: zo
|
||||||
|
// ontstaat een schuine bovenzijde. Onderin blijft het altijd waterpas
|
||||||
|
// (ZW/ZO: breedte + onderkant).
|
||||||
// ------------------------------------------------------------
|
// ------------------------------------------------------------
|
||||||
let selectedOpeningId = null;
|
let selectedOpeningId = null;
|
||||||
let dragInfo = null; // { type:'move'|'resize', handle, openingId, startMouseX, startMouseY, startLocalX, startY, startW, startH }
|
let dragInfo = null; // { type:'move'|'resize', handle, openingId, startMouseX, startMouseY, startLocalX, startY, startW, startH, startHR }
|
||||||
let frontView = { scale: 1, centerX: 0, floorY: 0 }; // bijgewerkt door draw2DFront()
|
let frontView = { scale: 1, centerX: 0, floorY: 0 }; // bijgewerkt door draw2DFront()
|
||||||
let frontOpeningRects = []; // hit-test data, bijgewerkt door draw2DFront()
|
let frontOpeningRects = []; // hit-test data, bijgewerkt door draw2DFront()
|
||||||
let frontHandleRects = []; // idem, alleen voor de geselecteerde opening
|
let frontHandleRects = []; // idem, alleen voor de geselecteerde opening
|
||||||
@@ -315,10 +320,17 @@ function mountWallEditor(container, callbacks) {
|
|||||||
op.w = w;
|
op.w = w;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSloped = (op) => op.hR !== undefined;
|
||||||
|
// Ruwe opening zoals opgeslagen (hR alleen bij een schuine bovenzijde).
|
||||||
|
const rawOpening = (o) => Object.assign({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset },
|
||||||
|
o.hR !== undefined ? { hR: o.hR } : {});
|
||||||
|
|
||||||
function syncOpeningCardInputs(op) {
|
function syncOpeningCardInputs(op) {
|
||||||
const card = document.querySelector(`.opening-card[data-opening-id="${op.id}"]`);
|
const card = document.querySelector(`.opening-card[data-opening-id="${op.id}"]`);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
['w', 'h', 'y', 'offset'].forEach(field => {
|
// Schuin aan/uit veranderd (hoek slepen): kaart opnieuw opbouwen.
|
||||||
|
if (!!card.querySelector('input[data-field="hR"]') !== isSloped(op)) { renderOpeningsList(); return; }
|
||||||
|
['w', 'h', 'hR', 'y', 'offset'].forEach(field => {
|
||||||
const input = card.querySelector(`input[data-field="${field}"]`);
|
const input = card.querySelector(`input[data-field="${field}"]`);
|
||||||
if (input && document.activeElement !== input) input.value = Math.round(op[field]);
|
if (input && document.activeElement !== input) input.value = Math.round(op[field]);
|
||||||
});
|
});
|
||||||
@@ -429,6 +441,8 @@ function mountWallEditor(container, callbacks) {
|
|||||||
hRightInput.value = tmp;
|
hRightInput.value = tmp;
|
||||||
|
|
||||||
openings = openings.map(o => {
|
openings = openings.map(o => {
|
||||||
|
// Schuine bovenzijde klapt mee om.
|
||||||
|
if (isSloped(o)) o = { ...o, h: o.hR, hR: o.h };
|
||||||
if (o.anchor === 'left') return { ...o, anchor: 'right' };
|
if (o.anchor === 'left') return { ...o, anchor: 'right' };
|
||||||
if (o.anchor === 'right') return { ...o, anchor: 'left' };
|
if (o.anchor === 'right') return { ...o, anchor: 'left' };
|
||||||
if (o.anchor === 'middle') return { ...o, offset: -o.offset };
|
if (o.anchor === 'middle') return { ...o, offset: -o.offset };
|
||||||
@@ -463,9 +477,13 @@ function mountWallEditor(container, callbacks) {
|
|||||||
<div><input type="number" data-field="w" data-id="${op.id}" value="${op.w}" step="50" min="400" max="2850"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="w" data-id="${op.id}" value="${op.w}" step="50" min="400" max="2850"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Hoogte</label>
|
<label>${isSloped(op) ? 'Hoogte links' : 'Hoogte'}</label>
|
||||||
<div><input type="number" data-field="h" data-id="${op.id}" value="${op.h}" step="50" min="400"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="h" data-id="${op.id}" value="${op.h}" step="50" min="400"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
${isSloped(op) ? `<div class="control-group">
|
||||||
|
<label>Hoogte rechts</label>
|
||||||
|
<div><input type="number" data-field="hR" data-id="${op.id}" value="${op.hR}" step="50" min="400"><span class="unit">mm</span></div>
|
||||||
|
</div>` : ''}
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<label>Afstand tot vloer</label>
|
<label>Afstand tot vloer</label>
|
||||||
<div><input type="number" data-field="y" data-id="${op.id}" value="${op.y}" step="50" min="0"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="y" data-id="${op.id}" value="${op.y}" step="50" min="0"><span class="unit">mm</span></div>
|
||||||
@@ -486,6 +504,11 @@ function mountWallEditor(container, callbacks) {
|
|||||||
<label>${anchorOffsetLabel(op.anchor)}</label>
|
<label>${anchorOffsetLabel(op.anchor)}</label>
|
||||||
<div><input type="number" data-field="offset" data-id="${op.id}" value="${op.offset}" step="50"><span class="unit">mm</span></div>
|
<div><input type="number" data-field="offset" data-id="${op.id}" value="${op.offset}" step="50"><span class="unit">mm</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control-group">
|
||||||
|
<label>Schuine bovenzijde</label>
|
||||||
|
<div><label class="ui-switch"><input type="checkbox" data-field="sloped" data-id="${op.id}" ${isSloped(op) ? 'checked' : ''}><span></span></label></div>
|
||||||
|
</div>
|
||||||
|
${isSloped(op) ? `<button class="opening-roof-btn" data-action="follow-roof" data-id="${op.id}" title="Hoogte rechts zo zetten dat de bovenkant evenwijdig aan de daklijn loopt"><i class="bi bi-slash-lg"></i> Volg dakhelling</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="opening-warning-text" data-warning-for="${op.id}" style="display:none;"></div>
|
<div class="opening-warning-text" data-warning-for="${op.id}" style="display:none;"></div>
|
||||||
@@ -519,6 +542,10 @@ function mountWallEditor(container, callbacks) {
|
|||||||
const preset = WallModel.OPENING_TYPES[e.target.value];
|
const preset = WallModel.OPENING_TYPES[e.target.value];
|
||||||
op.type = e.target.value;
|
op.type = e.target.value;
|
||||||
op.w = preset.w; op.h = preset.h; op.y = preset.y;
|
op.w = preset.w; op.h = preset.h; op.y = preset.y;
|
||||||
|
delete op.hR;
|
||||||
|
renderOpeningsList();
|
||||||
|
} else if (field === 'sloped') {
|
||||||
|
if (e.target.checked) op.hR = op.h; else delete op.hR;
|
||||||
renderOpeningsList();
|
renderOpeningsList();
|
||||||
} else if (field === 'anchor') {
|
} else if (field === 'anchor') {
|
||||||
op.anchor = e.target.value;
|
op.anchor = e.target.value;
|
||||||
@@ -532,9 +559,24 @@ function mountWallEditor(container, callbacks) {
|
|||||||
if (e.target.dataset.action === 'remove') {
|
if (e.target.dataset.action === 'remove') {
|
||||||
removeOpening(parseInt(e.target.dataset.id, 10));
|
removeOpening(parseInt(e.target.dataset.id, 10));
|
||||||
}
|
}
|
||||||
|
const roofBtn = e.target.closest('[data-action="follow-roof"]');
|
||||||
|
if (roofBtn) followRoof(parseInt(roofBtn.dataset.id, 10));
|
||||||
});
|
});
|
||||||
document.getElementById('btn-add-opening').addEventListener('click', addOpening);
|
document.getElementById('btn-add-opening').addEventListener('click', addOpening);
|
||||||
|
|
||||||
|
// Bovenkant evenwijdig aan de daklijn: hoogte rechts = hoogte links +
|
||||||
|
// het verschil in daklijn tussen de rechter- en linkerrand (op de
|
||||||
|
// opgeloste positie; met een nok ertussen het rechte stuk ertussen).
|
||||||
|
function followRoof(id) {
|
||||||
|
const op = openings.find(o => o.id === id);
|
||||||
|
const res = (CONFIG.openings || []).find(o => o.id === id);
|
||||||
|
if (!op || !res) return;
|
||||||
|
const at = WallModel.topLine(CONFIG).at, x0 = -CONFIG.wallWidth / 2 + res.x;
|
||||||
|
op.hR = Math.max(400, Math.round(op.h + at(x0 + res.w) - at(x0)));
|
||||||
|
syncOpeningCardInputs(op);
|
||||||
|
updateAll();
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// JSON EXPORT / IMPORT — sla het volledige wand-ontwerp (parameters +
|
// JSON EXPORT / IMPORT — sla het volledige wand-ontwerp (parameters +
|
||||||
// ruwe, niet-opgeloste openingen) op als JSON, en laad het weer terug.
|
// ruwe, niet-opgeloste openingen) op als JSON, en laad het weer terug.
|
||||||
@@ -569,7 +611,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
return {
|
return {
|
||||||
wall: collectWallParams(),
|
wall: collectWallParams(),
|
||||||
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone },
|
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone },
|
||||||
openings: openings.map(o => ({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset }))
|
openings: openings.map(rawOpening)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,7 +638,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
stone = (state.materials && state.materials.stone) || null; // woning-instelling (steen)
|
stone = (state.materials && state.materials.stone) || null; // woning-instelling (steen)
|
||||||
syncMaterialButtons();
|
syncMaterialButtons();
|
||||||
|
|
||||||
openings = state.openings.map(o => ({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset }));
|
openings = state.openings.map(rawOpening);
|
||||||
openingIdCounter = openings.length ? Math.max(...openings.map(o => o.id)) + 1 : 1;
|
openingIdCounter = openings.length ? Math.max(...openings.map(o => o.id)) + 1 : 1;
|
||||||
selectedOpeningId = null;
|
selectedOpeningId = null;
|
||||||
|
|
||||||
@@ -980,39 +1022,53 @@ function mountWallEditor(container, callbacks) {
|
|||||||
const x0 = toCanvasX(opLeftMM);
|
const x0 = toCanvasX(opLeftMM);
|
||||||
const x1 = toCanvasX(opLeftMM + op.w);
|
const x1 = toCanvasX(opLeftMM + op.w);
|
||||||
const y0 = toCanvasY(op.y);
|
const y0 = toCanvasY(op.y);
|
||||||
const y1 = toCanvasY(op.y + op.h);
|
const y1 = toCanvasY(op.y + op.h); // bovenkant links
|
||||||
|
const y1r = toCanvasY(op.y + op.hR); // bovenkant rechts (schuine bovenzijde)
|
||||||
const isSelected = op.id === selectedOpeningId;
|
const isSelected = op.id === selectedOpeningId;
|
||||||
|
|
||||||
frontOpeningRects.push({ id: op.id, x0, y0, x1, y1, localX: op.x, y: op.y, w: op.w, h: op.h });
|
frontOpeningRects.push({ id: op.id, x0, y0, x1, y1, y1r, localX: op.x, y: op.y, w: op.w, h: op.h, hR: op.hR });
|
||||||
|
|
||||||
|
const outline = () => {
|
||||||
|
ctxFront.beginPath();
|
||||||
|
ctxFront.moveTo(x0, y0); ctxFront.lineTo(x1, y0); ctxFront.lineTo(x1, y1r); ctxFront.lineTo(x0, y1);
|
||||||
|
ctxFront.closePath();
|
||||||
|
};
|
||||||
|
outline();
|
||||||
ctxFront.fillStyle = op.overflowing ? 'rgba(255, 60, 60, 0.45)' : 'rgba(255, 50, 50, 0.25)';
|
ctxFront.fillStyle = op.overflowing ? 'rgba(255, 60, 60, 0.45)' : 'rgba(255, 50, 50, 0.25)';
|
||||||
ctxFront.fillRect(x0, y1, x1 - x0, y0 - y1);
|
ctxFront.fill();
|
||||||
ctxFront.strokeStyle = isSelected ? '#4da6ff' : '#ff3333';
|
ctxFront.strokeStyle = isSelected ? '#4da6ff' : '#ff3333';
|
||||||
ctxFront.lineWidth = isSelected ? 2.5 : 1;
|
ctxFront.lineWidth = isSelected ? 2.5 : 1;
|
||||||
ctxFront.setLineDash(isSelected ? [] : [3, 3]);
|
ctxFront.setLineDash(isSelected ? [] : [3, 3]);
|
||||||
ctxFront.strokeRect(x0, y1, x1 - x0, y0 - y1);
|
ctxFront.stroke();
|
||||||
ctxFront.setLineDash([]);
|
ctxFront.setLineDash([]);
|
||||||
ctxFront.lineWidth = 1;
|
ctxFront.lineWidth = 1;
|
||||||
|
|
||||||
ctxFront.fillStyle = '#fff';
|
ctxFront.fillStyle = '#fff';
|
||||||
ctxFront.font = '9px sans-serif';
|
ctxFront.font = '9px sans-serif';
|
||||||
ctxFront.textAlign = 'center';
|
ctxFront.textAlign = 'center';
|
||||||
ctxFront.fillText('#' + op.id, (x0 + x1) / 2, y1 - 4);
|
ctxFront.fillText('#' + op.id, (x0 + x1) / 2, (y1 + y1r) / 2 - 4);
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const midX = (x0 + x1) / 2, midY = (y0 + y1) / 2;
|
const midX = (x0 + x1) / 2, midTop = (y1 + y1r) / 2;
|
||||||
const handles = [
|
const handles = [
|
||||||
{ handle: 'n', cx: midX, cy: y1 },
|
{ handle: 'nw', cx: x0, cy: y1 },
|
||||||
|
{ handle: 'n', cx: midX, cy: midTop },
|
||||||
|
{ handle: 'ne', cx: x1, cy: y1r },
|
||||||
|
{ handle: 'w', cx: x0, cy: (y0 + y1) / 2 },
|
||||||
|
{ handle: 'c', cx: midX, cy: (y0 + midTop) / 2 },
|
||||||
|
{ handle: 'e', cx: x1, cy: (y0 + y1r) / 2 },
|
||||||
|
{ handle: 'sw', cx: x0, cy: y0 },
|
||||||
{ handle: 's', cx: midX, cy: y0 },
|
{ handle: 's', cx: midX, cy: y0 },
|
||||||
{ handle: 'e', cx: x1, cy: midY },
|
{ handle: 'se', cx: x1, cy: y0 }
|
||||||
{ handle: 'w', cx: x0, cy: midY }
|
|
||||||
];
|
];
|
||||||
handles.forEach(hd => {
|
handles.forEach(hd => {
|
||||||
frontHandleRects.push({ id: op.id, handle: hd.handle, cx: hd.cx, cy: hd.cy });
|
frontHandleRects.push({ id: op.id, handle: hd.handle, cx: hd.cx, cy: hd.cy });
|
||||||
ctxFront.fillStyle = '#4da6ff';
|
ctxFront.fillStyle = hd.handle === 'c' ? '#ffffff' : '#4da6ff';
|
||||||
ctxFront.strokeStyle = '#0a1a2a';
|
ctxFront.strokeStyle = '#0a1a2a';
|
||||||
ctxFront.beginPath();
|
ctxFront.beginPath();
|
||||||
ctxFront.rect(hd.cx - 5, hd.cy - 5, 10, 10);
|
// Midden (verplaatsen) rond, de rest vierkant.
|
||||||
|
if (hd.handle === 'c') ctxFront.arc(hd.cx, hd.cy, 5, 0, Math.PI * 2);
|
||||||
|
else ctxFront.rect(hd.cx - 5, hd.cy - 5, 10, 10);
|
||||||
ctxFront.fill();
|
ctxFront.fill();
|
||||||
ctxFront.stroke();
|
ctxFront.stroke();
|
||||||
});
|
});
|
||||||
@@ -1062,9 +1118,12 @@ function mountWallEditor(container, callbacks) {
|
|||||||
return frontHandleRects.find(hd => Math.abs(hd.cx - mx) <= 8 && Math.abs(hd.cy - my) <= 8);
|
return frontHandleRects.find(hd => Math.abs(hd.cx - mx) <= 8 && Math.abs(hd.cy - my) <= 8);
|
||||||
}
|
}
|
||||||
function frontOpeningAt(mx, my) {
|
function frontOpeningAt(mx, my) {
|
||||||
return frontOpeningRects.find(r =>
|
// Binnen de (evt. schuine) omtrek: canvas-y loopt omlaag.
|
||||||
mx >= Math.min(r.x0, r.x1) && mx <= Math.max(r.x0, r.x1) &&
|
return frontOpeningRects.find(r => {
|
||||||
my >= Math.min(r.y0, r.y1) && my <= Math.max(r.y0, r.y1));
|
if (mx < r.x0 || mx > r.x1) return false;
|
||||||
|
const yTop = r.y1 + (mx - r.x0) / ((r.x1 - r.x0) || 1) * (r.y1r - r.y1);
|
||||||
|
return my <= r.y0 && my >= yTop;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas2DFront.classList.add('interactive');
|
canvas2DFront.classList.add('interactive');
|
||||||
@@ -1077,9 +1136,9 @@ function mountWallEditor(container, callbacks) {
|
|||||||
if (hit) {
|
if (hit) {
|
||||||
const resolved = frontOpeningRects.find(r => r.id === hit.id);
|
const resolved = frontOpeningRects.find(r => r.id === hit.id);
|
||||||
dragInfo = {
|
dragInfo = {
|
||||||
type: 'resize', handle: hit.handle, openingId: hit.id,
|
type: hit.handle === 'c' ? 'move' : 'resize', handle: hit.handle, openingId: hit.id,
|
||||||
startMouseX: e.clientX, startMouseY: e.clientY,
|
startMouseX: e.clientX, startMouseY: e.clientY,
|
||||||
startLocalX: resolved.localX, startY: resolved.y, startW: resolved.w, startH: resolved.h
|
startLocalX: resolved.localX, startY: resolved.y, startW: resolved.w, startH: resolved.h, startHR: resolved.hR
|
||||||
};
|
};
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
@@ -1092,7 +1151,7 @@ function mountWallEditor(container, callbacks) {
|
|||||||
dragInfo = {
|
dragInfo = {
|
||||||
type: 'move', openingId: bodyHit.id,
|
type: 'move', openingId: bodyHit.id,
|
||||||
startMouseX: e.clientX, startMouseY: e.clientY,
|
startMouseX: e.clientX, startMouseY: e.clientY,
|
||||||
startLocalX: bodyHit.localX, startY: bodyHit.y, startW: bodyHit.w, startH: bodyHit.h
|
startLocalX: bodyHit.localX, startY: bodyHit.y, startW: bodyHit.w, startH: bodyHit.h, startHR: bodyHit.hR
|
||||||
};
|
};
|
||||||
highlightSelectedCard();
|
highlightSelectedCard();
|
||||||
draw2D();
|
draw2D();
|
||||||
@@ -1121,17 +1180,32 @@ function mountWallEditor(container, callbacks) {
|
|||||||
applyOpeningPosition(op, newLocalX, dragInfo.startW, wallWidth);
|
applyOpeningPosition(op, newLocalX, dragInfo.startW, wallWidth);
|
||||||
op.y = Math.max(0, snap50(dragInfo.startY + dyMM));
|
op.y = Math.max(0, snap50(dragInfo.startY + dyMM));
|
||||||
} else if (dragInfo.type === 'resize') {
|
} else if (dragInfo.type === 'resize') {
|
||||||
if (dragInfo.handle === 'n') {
|
// Samengestelde handles: 'nw' = bovenkant links + linkerrand, enz.
|
||||||
op.h = Math.max(400, snap50(dragInfo.startH + dyMM));
|
const hd = dragInfo.handle;
|
||||||
} else if (dragInfo.handle === 's') {
|
const sloped = dragInfo.startHR !== dragInfo.startH || isSloped(op);
|
||||||
const top = dragInfo.startY + dragInfo.startH;
|
if (hd === 'n') {
|
||||||
const newY = Math.min(snap50(dragInfo.startY + dyMM), top - 400);
|
// Hele bovenkant omhoog/omlaag (helling blijft).
|
||||||
|
const dh = snap50(dyMM);
|
||||||
|
op.h = Math.max(400, dragInfo.startH + dh);
|
||||||
|
if (sloped) op.hR = Math.max(400, dragInfo.startHR + dh);
|
||||||
|
} else if (hd === 'nw' || hd === 'ne') {
|
||||||
|
// Eén bovenhoek: alleen die kant in de hoogte -> schuine bovenzijde.
|
||||||
|
op.h = hd === 'nw' ? Math.max(400, snap50(dragInfo.startH + dyMM)) : dragInfo.startH;
|
||||||
|
op.hR = hd === 'ne' ? Math.max(400, snap50(dragInfo.startHR + dyMM)) : dragInfo.startHR;
|
||||||
|
if (op.h === op.hR) delete op.hR; // weer recht
|
||||||
|
}
|
||||||
|
if (hd === 's' || hd === 'sw' || hd === 'se') {
|
||||||
|
// Onderkant; de bovenkant blijft staan (onderin altijd waterpas).
|
||||||
|
const topL = dragInfo.startY + dragInfo.startH, topR = dragInfo.startY + dragInfo.startHR;
|
||||||
|
const newY = Math.min(snap50(dragInfo.startY + dyMM), Math.min(topL, topR) - 400);
|
||||||
op.y = Math.max(0, newY);
|
op.y = Math.max(0, newY);
|
||||||
op.h = top - op.y;
|
op.h = topL - op.y;
|
||||||
} else if (dragInfo.handle === 'e') {
|
if (isSloped(op)) op.hR = topR - op.y;
|
||||||
|
}
|
||||||
|
if (hd === 'e' || hd === 'ne' || hd === 'se') {
|
||||||
const newW = Math.max(400, snap50(dragInfo.startW + dxMM));
|
const newW = Math.max(400, snap50(dragInfo.startW + dxMM));
|
||||||
applyOpeningPosition(op, dragInfo.startLocalX, newW, wallWidth);
|
applyOpeningPosition(op, dragInfo.startLocalX, newW, wallWidth);
|
||||||
} else if (dragInfo.handle === 'w') {
|
} else if (hd === 'w' || hd === 'nw' || hd === 'sw') {
|
||||||
const right = dragInfo.startLocalX + dragInfo.startW;
|
const right = dragInfo.startLocalX + dragInfo.startW;
|
||||||
const newLocalX = Math.min(snap50(dragInfo.startLocalX + dxMM), right - 400);
|
const newLocalX = Math.min(snap50(dragInfo.startLocalX + dxMM), right - 400);
|
||||||
applyOpeningPosition(op, newLocalX, right - newLocalX, wallWidth);
|
applyOpeningPosition(op, newLocalX, right - newLocalX, wallWidth);
|
||||||
|
|||||||
Reference in New Issue
Block a user