v0.4.13: opbouw buitenwand boven lager dak, latwerk-raster per gevel, dakrand 80 mm boven dakleer, lage rand loopt door

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 15:33:47 +02:00
co-authored by Claude Opus 5.5
parent 373c2c2fb7
commit 94a5215f82
8 changed files with 207 additions and 46 deletions
+102 -22
View File
@@ -318,8 +318,18 @@ window.HouseModel = (() => {
const sLeft = dir > 0 ? run.s0 : run.s1, sRight = dir > 0 ? run.s1 : run.s0;
const first = run.segs.reduce((best, sg) => Math.abs(sg.sAt0 - sLeft) < Math.abs(best.sAt0 - sLeft) ? sg : best);
const extLeft = (first.piece.skinExt && first.piece.skinExt.left) || 0;
// Horizontaal latten-raster: vanaf het trasraam van een gewoon stuk
// (absolute hoogte), zodat opbouw/naadwand-stukken erop aansluiten.
const ref = run.segs.find(sg => !sg.piece.place.baseY && sg.piece.skinFrom === undefined);
const gridAbs = ref ? ref.piece.place.floorTop + WallModel.skinStartY(ref.piece.trasraam) : undefined;
const floorAbs = ref ? ref.piece.place.floorTop : undefined;
run.segs.forEach(sg => {
sg.piece.facade = { u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5 };
const p = sg.piece;
sg.piece.facade = {
u0: Math.abs(sg.sAt0 - sLeft) + extLeft, extStart: extLeft > 0, isEnd: Math.abs(sg.sAtW - sRight) < 5,
gridY0: gridAbs === undefined ? undefined : gridAbs - p.place.floorTop,
yOff: floorAbs === undefined ? 0 : p.place.floorTop - floorAbs
};
});
});
}
@@ -343,6 +353,38 @@ window.HouseModel = (() => {
return { group: built.group, stats: built.stats, specs: WallModel.computeSpecs(config, built.stats, buitenMateriaal) };
}
// Wandstukken van één zijde (UnitModel.wallPieces), met plaats in de
// woning. raised: opbouw-zones (zie raiseSeamWalls). Een opbouwstuk
// (baseY) staat op die hoogte boven het loopvlak.
function wallOf(u, rect, layout, wl, raised) {
const ws = u.unit.walls[wl.side];
const buitenMateriaal = (ws.materials || {}).exterior || 'vochtdoek';
const cladding = (ws.materials || {}).cladding || 'horizontaal';
const trasraam = buitenMateriaal === 'vochtdoek' ? ((ws.materials || {}).trasraam || null) : null;
const wp = UnitModel.wallPieces(wl, ws, raised);
const pieces = wp.pieces.map(p => {
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
const base = p.baseY || 0;
return Object.assign(p, {
element: `${u.id}-W${wl.nr}${p.suffix}`,
label: `Unit ${u.id} ${wl.label.toLowerCase()}` + (base ? ` opbouw (vanaf ${base} mm)` : p.suffix ? ` (${p.suffix})` : ''),
buitenMateriaal, cladding, trasraam: base ? null : trasraam,
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop + base, baseY: base }
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
});
// Naad-zones met opbouw: in de wand-editor alleen gearceerd tot base.
// Naast de opbouw (haakse wand in de hoek) blijft het over de volle hoogte.
const cutsU = [];
wl.cutsU.forEach(([a, b]) => {
const r = pieces.find(p => p.baseY && p.u0 < b - EPS && p.u1 > a + EPS);
if (!r) { cutsU.push([a, b]); return; }
if (r.u0 > a + EPS) cutsU.push([a, r.u0]);
cutsU.push([Math.max(a, r.u0), Math.min(b, r.u1), r.baseY]);
if (r.u1 < b - EPS) cutsU.push([r.u1, b]);
});
return Object.assign({}, wl, { cutsU, pieces, dropped: wp.dropped, removed: pieces.length === 0 });
}
const units = state.units.map(u => {
const rect = rectById[u.id];
const layout = UnitModel.computeLayout(u.unit, cuts[u.id]);
@@ -355,23 +397,7 @@ window.HouseModel = (() => {
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false
}) : null;
const walls = layout.walls.map(wl => {
const ws = u.unit.walls[wl.side];
const buitenMateriaal = (ws.materials || {}).exterior || 'vochtdoek';
const cladding = (ws.materials || {}).cladding || 'horizontaal';
const trasraam = buitenMateriaal === 'vochtdoek' ? ((ws.materials || {}).trasraam || null) : null;
const wp = UnitModel.wallPieces(wl, ws);
const pieces = wp.pieces.map(p => {
const c = UnitModel.localToWorld(wl, p.lxCenter, 0);
return Object.assign(p, {
element: `${u.id}-W${wl.nr}${p.suffix}`,
label: `Unit ${u.id} ${wl.label.toLowerCase()}${p.suffix ? ' (' + p.suffix + ')' : ''}`,
buitenMateriaal, cladding, trasraam,
place: { cx: rect.cx + c.x, cz: rect.cz + c.z, rotY: wl.rotY, width: p.config.wallWidth, thickness: wl.thickness, normal: wl.normal, floorTop: layout.floorTop }
}); // 3D volgt na assignFacades (latwerk/doek per gevel)
});
return Object.assign({}, wl, { pieces, dropped: wp.dropped, removed: pieces.length === 0 });
});
const walls = layout.walls.map(wl => wallOf(u, rect, layout, wl));
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape).
@@ -412,6 +438,55 @@ window.HouseModel = (() => {
};
});
// Opbouw op een naad: staat een unit met een hogere daklijn tegen een
// unit met een lager dak, dan loopt zijn buitenwand aan de naadzijde door
// vanaf de bovenkant van het lage dak tot zijn eigen daklijn (zoals een
// buitenwand die bij een schuin dak omhoog groeit). Ramen daar teken je
// op die buitenwand. De naadwand eronder stopt op die hoogte (raisedCap).
// Alleen bij een rechte voet: het lage dak ligt langs de hele naad even
// hoog; anders blijft de naadwand doorlopen met buitenafwerking.
const raisedCap = {}; // `${uid}|${andere uid}` -> base (mm vanaf loopvlak uid)
(function raiseSeamWalls() {
const zones = {}; // uid -> side -> [{ u0, u1, base, other }]
seams.forEach(sm => {
const horiz = Math.abs(sm.z0 - sm.z1) < EPS;
const seamPerp = horiz ? sm.z0 : sm.x0;
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1;
[[sm.a, sm.b, horiz ? 'achter' : 'rechts'], [sm.b, sm.a, horiz ? 'voor' : 'links']].forEach(([uid, oid, side]) => {
const u = units.find(x => x.id === uid), other = units.find(x => x.id === oid);
const ts = [s0, s1].concat(other.roof.ridges.filter(r => r.axis === (horiz ? 'x' : 'z') && r.value > s0 && r.value < s1).map(r => r.value));
const tops = ts.map(t => horiz ? other.roof.topAt(t, seamPerp) : other.roof.topAt(seamPerp, t));
if (Math.max(...tops) - Math.min(...tops) > 2) return; // schuine voet: terugval
const base = Math.round(tops[0] - u.layout.floorTop);
const wl = u.walls.find(w => w.side === side);
const sd = UnitModel.WALL_SIDES.find(x => x.key === side);
const c0 = sd.axis === 'x' ? u.rect.cx : u.rect.cz;
const start = (sd.axis === 'x' ? wl.cx : wl.cz) - wl.width / 2;
const toU = (t) => sd.dir > 0 ? t - c0 - start : start + wl.width - (t - c0);
const z0 = Math.min(toU(s0), toU(s1)), z1 = Math.max(toU(s0), toU(s1));
wl.cutsU.forEach(([a, b]) => {
let lo = Math.max(a, z0), hi = Math.min(b, z1);
if (lo < EPS) lo = wl.endInset.left;
if (hi > wl.width - EPS) hi = wl.width - wl.endInset.right;
if (hi - lo > EPS) ((zones[uid] = zones[uid] || {})[side] = zones[uid][side] || []).push({ u0: lo, u1: hi, base, other: oid });
});
});
});
Object.keys(zones).forEach(uid => {
const u = units.find(x => x.id === uid);
const su = state.units.find(x => x.id === uid);
Object.keys(zones[uid]).forEach(side => {
const i = u.walls.findIndex(w => w.side === side);
const wl = u.layout.walls.find(w => w.side === side);
const next = wallOf(su, u.rect, u.layout, wl, zones[uid][side]);
u.walls[i] = next;
zones[uid][side].forEach(z => {
if (next.pieces.some(p => p.baseY && Math.abs(p.u0 - z.u0) < EPS)) raisedCap[`${uid}|${z.other}`] = z.base;
});
});
});
})();
// Binnenwanden staan op de vloer van de unit waarin hun midden valt.
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
const interior = [];
@@ -455,8 +530,9 @@ window.HouseModel = (() => {
// de naad aan, met de hoogte van de daklijn van die unit.
const inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
const T2 = WallModel.thicknessOf(baseConfig, 'spaanplaat');
// Opbouwstukken (baseY) staan boven de naadwand, niet ernaast.
const extPieces = [];
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => extPieces.push(p))));
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => { if (!p.baseY) extPieces.push(p); })));
const along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
const seamPerp = horiz ? sm.z0 : sm.x0;
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1;
@@ -500,10 +576,14 @@ window.HouseModel = (() => {
const W = lf.place.width;
const pL = axisOf(UnitModel.localToWorld(lf.place, -W / 2, 0)), pR = axisOf(UnitModel.localToWorld(lf.place, W / 2, 0));
const peak = prof.length > 2 ? prof[1] : null;
const hasPeak = !!peak && peak.p > Math.min(pL, pR) + 1 && peak.p < Math.max(pL, pR) - 1;
// Opbouw erboven (raiseSeamWalls): het deel stopt op de voet daarvan.
const cap = raisedCap[`${lf.uid}|${lf.uid === sm.a ? sm.b : sm.a}`];
const lim = (h) => cap !== undefined ? Math.min(h, cap) : h;
const hL = lim(RoofModel.profileHeight(prof, pL)), hR = lim(RoofModel.profileHeight(prof, pR));
const hasPeak = !!peak && peak.p > Math.min(pL, pR) + 1 && peak.p < Math.max(pL, pR) - 1 && lim(peak.h) > Math.max(hL, hR) + 0.5;
return {
heightLeft: Math.round(RoofModel.profileHeight(prof, pL)), heightRight: Math.round(RoofModel.profileHeight(prof, pR)),
heightMid: hasPeak ? Math.round(peak.h) : null, peakX: hasPeak ? Math.abs(peak.p - pL) : undefined
heightLeft: Math.round(hL), heightRight: Math.round(hR),
heightMid: hasPeak ? Math.round(lim(peak.h)) : null, peakX: hasPeak ? Math.abs(peak.p - pL) : undefined
};
};
const paramsOf = (lf) => Object.assign({}, iw.wall.wall, { width: lf.place.width }, heightsOf(lf));