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
+9 -4
View File
@@ -59,9 +59,12 @@ assets/ logo, texturen, skybox
- **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op - **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); op
naden vervallen de buitenwanden en komt een naadwand met deur: midden op naden vervallen de buitenwanden en komt een naadwand met deur: midden op
de naad in twee delen (één per unit), of als één binnenwand geheel in unit de naad in twee delen (één per unit), of als één binnenwand geheel in unit
A of B. De hoogte volgt per deel de daklijn van zijn unit; steekt een deel A of B. De hoogte volgt per deel de daklijn van zijn unit. Staat een unit
boven het dak van de andere unit uit, dan krijgt dat stuk buitenafwerking met een hogere daklijn tegen een lager dak, dan krijgt zijn buitenwand aan
(vochtdoek + latwerk) die met de gevel meeloopt. de naadzijde een opbouw vanaf de bovenkant van het lage dak (ramen daar
teken je op die buitenwand) en stopt de naadwand op die hoogte. Loopt het
lage dak langs de naad schuin, dan loopt de naadwand door en krijgt het
uitstekende stuk buitenafwerking (vochtdoek + latwerk).
Binnenwanden teken je in de plattegrond. Binnenwanden teken je in de plattegrond.
- **Dak:** per unit opgebouwd als gespiegelde vloer + dakleer, in de vorm - **Dak:** per unit opgebouwd als gespiegelde vloer + dakleer, in de vorm
die de wanden aangeven: plat, lessenaar (schuine kopgevels) of zadel die de wanden aangeven: plat, lessenaar (schuine kopgevels) of zadel
@@ -70,7 +73,9 @@ assets/ logo, texturen, skybox
(boeidelen/daktrim, spots) loopt als één rand door en volgt de helling. (boeidelen/daktrim, spots) loopt als één rand door en volgt de helling.
Is een dak langs de naad minstens een boeideelhoogte hoger dan het Is een dak langs de naad minstens een boeideelhoogte hoger dan het
aangrenzende, dan krijgt het een eigen rand rondom (boven het lage dak aangrenzende, dan krijgt het een eigen rand rondom (boven het lage dak
zonder spots) en loopt de rand van het lage dak door tot de gevel. zonder spots). De rand van het lage dak loopt dan door tot de gevel van
het hoge, of langs een zijgevel tot de volle overstek (onder de hoge
rand). Boeideel en daktrim steken 80 mm boven het dakleer uit.
- **Gevel:** latwerk en vochtdoek lopen per gevel door over unit-naden; - **Gevel:** latwerk en vochtdoek lopen per gevel door over unit-naden;
sandwichpanelen rondom, 600 mm de grond in. sandwichpanelen rondom, 600 mm de grond in.
- **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld, - **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld,
+1 -1
View File
@@ -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.12'; const APP_VERSION = '0.4.13';
// 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 = [];
+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 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 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; 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 => { 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) }; 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 units = state.units.map(u => {
const rect = rectById[u.id]; const rect = rectById[u.id];
const layout = UnitModel.computeLayout(u.unit, cuts[u.id]); const layout = UnitModel.computeLayout(u.unit, cuts[u.id]);
@@ -355,23 +397,7 @@ window.HouseModel = (() => {
materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false materials: o.floorMaterials, osbVisible: o.beplatingVisible !== false
}) : null; }) : null;
const walls = layout.walls.map(wl => { const walls = layout.walls.map(wl => wallOf(u, rect, layout, 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 });
});
// Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die // Dak: dek = vloer-state (180° gespiegeld, RoofModel), in de vorm die
// de wanden aangeven (plat, lessenaar of zadel; RoofModel.computeShape). // 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. // Binnenwanden staan op de vloer van de unit waarin hun midden valt.
// Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves). // Naadwanden (iw.seam) leveren twee delen op, één per unit (seamLeaves).
const interior = []; const interior = [];
@@ -455,8 +530,9 @@ window.HouseModel = (() => {
// de naad aan, met de hoogte van de daklijn van die unit. // 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 inUnit = iw.seam.in === 'a' || iw.seam.in === 'b' ? iw.seam[iw.seam.in] : null;
const T2 = WallModel.thicknessOf(baseConfig, 'spaanplaat'); const T2 = WallModel.thicknessOf(baseConfig, 'spaanplaat');
// Opbouwstukken (baseY) staan boven de naadwand, niet ernaast.
const extPieces = []; 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 along = (q) => horiz ? q.x : q.z, perpOf = (q) => horiz ? q.z : q.x;
const seamPerp = horiz ? sm.z0 : sm.x0; const seamPerp = horiz ? sm.z0 : sm.x0;
const s0 = horiz ? sm.x0 : sm.z0, s1 = horiz ? sm.x1 : sm.z1; 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 W = lf.place.width;
const pL = axisOf(UnitModel.localToWorld(lf.place, -W / 2, 0)), pR = axisOf(UnitModel.localToWorld(lf.place, W / 2, 0)); 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 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 { return {
heightLeft: Math.round(RoofModel.profileHeight(prof, pL)), heightRight: Math.round(RoofModel.profileHeight(prof, pR)), heightLeft: Math.round(hL), heightRight: Math.round(hR),
heightMid: hasPeak ? Math.round(peak.h) : null, peakX: hasPeak ? Math.abs(peak.p - pL) : undefined 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)); const paramsOf = (lf) => Object.assign({}, iw.wall.wall, { width: lf.place.width }, heightsOf(lf));
+1
View File
@@ -119,6 +119,7 @@ window.LightingModel = (() => {
const segs = []; const segs = [];
units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => { units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => {
p.lamps = []; p.lamps = [];
if (p.baseY) return; // opbouw boven een lager dak: geen gevellampen
const W = p.place.width; const W = p.place.width;
// Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding. // Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding.
const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal); const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal);
+32 -11
View File
@@ -28,7 +28,9 @@
// dakrand = standaard de buitenkant gevel), standaard 300 mm hoog. // dakrand = standaard de buitenkant gevel), standaard 300 mm hoog.
// Spots in de onderplaat staan op 40% van de overstek (spotLine); welke // Spots in de onderplaat staan op 40% van de overstek (spotLine); welke
// spots er komen bepaalt LightingModel. // spots er komen bepaalt LightingModel.
// - zonder boeidelen: een daktrim van 80 mm hoog. // - zonder boeidelen: een daktrim van 80 mm hoog (onder het dakleer).
// De bovenkant van boeideel en daktrim ligt EDGE_ABOVE_MM boven het dakleer
// (opstand rondom), dus het dakleer ligt altijd lager dan de rand.
// //
// Plain script, geladen na floor-model.js en core/geometry.js. // Plain script, geladen na floor-model.js en core/geometry.js.
// ============================================================ // ============================================================
@@ -37,7 +39,7 @@ window.RoofModel = (() => {
const DAKLEER_MM = 2; // visuele dikte van het dakleer const DAKLEER_MM = 2; // visuele dikte van het dakleer
const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2 const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2
const TRIM_THICK_MM = 25; // daktrim-profiel, uitstekend buiten de dakrand const TRIM_THICK_MM = 25; // daktrim-profiel, uitstekend buiten de dakrand
const TRIM_ABOVE_MM = 30; // daktrim steekt zoveel boven het dakleer uit const EDGE_ABOVE_MM = 80; // bovenkant boeideel/daktrim ligt zoveel boven het dakleer
const SPOT_RATIO = 0.4; // spots op 40% van de overstek, gemeten vanaf de gevel const SPOT_RATIO = 0.4; // spots op 40% van de overstek, gemeten vanaf de gevel
const OVERHANG_MIN = 200, OVERHANG_MAX = 400; const OVERHANG_MIN = 200, OVERHANG_MAX = 400;
@@ -357,17 +359,35 @@ window.RoofModel = (() => {
return { return {
a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b), a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b),
outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j], outerA: off[i], outerB: off[j], spotA: offSpot[i], spotB: offSpot[j],
topAt: (p) => owner.topAt(p.x, p.z), ridges: owner.ridges || [], // Bovenkant van de rand (dakleer + opstand).
topAt: (p) => owner.topAt(p.x, p.z) + EDGE_ABOVE_MM, ridges: owner.ridges || [],
overRoof: !!nb && !dropped, dropped overRoof: !!nb && !dropped, dropped
}; };
}); });
// Naast een vervallen ribbe loopt de rand recht door tot de gevel. // Naast een vervallen ribbe (tegen een hoger dak):
// - ligt het verlengde van de rand in het hoge deel, dan loopt hij
// recht door tot die gevel (spot op spotafstand van de gevel);
// - ligt het verlengde ernaast (de rand loopt langs de zijgevel van
// het hoge deel), dan loopt hij recht door tot de overstek, onder
// de rand van het hoge dak (van bovenaf overlappen ze).
const insideOther = (p) => others.some(r => p.x > r.rect.x0 + 0.5 && p.x < r.rect.x1 - 0.5 && p.z > r.rect.z0 + 0.5 && p.z < r.rect.z1 - 0.5);
segs.forEach((sg, i) => { segs.forEach((sg, i) => {
if (sg.dropped) return;
const prev = segs[(i - 1 + n) % n], next = segs[(i + 1) % n]; const prev = segs[(i - 1 + n) % n], next = segs[(i + 1) % n];
const alongZ = Math.abs(sg.a.x - sg.b.x) < 0.5; const alongZ = Math.abs(sg.a.x - sg.b.x) < 0.5;
const square = (key, v) => { sg[key] = alongZ ? { x: sg[key].x, z: v.z } : { x: v.x, z: sg[key].z }; }; const at = (q, v) => alongZ ? { x: q.x, z: v.z } : { x: v.x, z: q.z };
if (!sg.dropped && prev.dropped) square('outerA', sg.a); [['a', 'outerA', 'spotA', 'b', prev], ['b', 'outerB', 'spotB', 'a', next]].forEach(([k, outerK, spotK, otherK, nb]) => {
if (!sg.dropped && next.dropped) square('outerB', sg.b); if (!nb.dropped) return;
const v = sg[k], corner = sg[outerK];
if (insideOther({ x: (v.x + corner.x) / 2, z: (v.z + corner.z) / 2 })) {
sg[outerK] = at(corner, v);
const dir = Math.sign(alongZ ? sg[otherK].z - v.z : sg[otherK].x - v.x);
const inset = dir * s.overhang * SPOT_RATIO;
sg[spotK] = alongZ ? { x: sg[spotK].x, z: v.z + inset } : { x: v.x + inset, z: sg[spotK].z };
} else {
sg[k] = at(v, corner);
}
});
}); });
segs.filter(sg => !sg.dropped).forEach(sg => segments.push(sg)); segs.filter(sg => !sg.dropped).forEach(sg => segments.push(sg));
}); });
@@ -377,7 +397,7 @@ window.RoofModel = (() => {
return { return {
segments, overhang: s.overhang, spotRatio: SPOT_RATIO, segments, overhang: s.overhang, spotRatio: SPOT_RATIO,
fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight, fasciaHeight: s.fasciaHeight, trimHeight: s.trimHeight,
top, trimAbove: TRIM_ABOVE_MM, top, edgeAbove: EDGE_ABOVE_MM,
fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0), fasciaLengthMM: segments.filter(sg => sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0), trimLengthMM: segments.filter(sg => !sg.fascia).reduce((t, sg) => t + segLen(sg), 0),
soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6, soffitM2: segments.filter(sg => sg.fascia).reduce((t, sg) => t + quadArea([sg.a, sg.b, sg.outerB, sg.outerA]), 0) / 1e6,
@@ -431,10 +451,11 @@ window.RoofModel = (() => {
const f = (p) => r.axis === 'x' ? p.x - r.value : p.z - r.value; const f = (p) => r.axis === 'x' ? p.x - r.value : p.z - r.value;
polys = polys.flatMap(pl => [clipPoly(pl, f), clipPoly(pl, p => -f(p))]).filter(pl => pl.length >= 3); polys = polys.flatMap(pl => [clipPoly(pl, f), clipPoly(pl, p => -f(p))]).filter(pl => pl.length >= 3);
}); });
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight; // Boeideel: totale hoogte fasciaHeight vanaf de bovenkant van de rand;
const topFn = sg.fascia ? sg.topAt : (p) => sg.topAt(p) + edge.trimAbove; // daktrim: trimHeight onder het dakleer + de opstand erboven.
const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove;
polys.forEach(pl => { polys.forEach(pl => {
const mesh = new THREE.Mesh(prismGeometry(pl, topFn, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial); const mesh = new THREE.Mesh(prismGeometry(pl, sg.topAt, h), sg.fascia ? materials.fasciaMaterial : materials.trimMaterial);
mesh.visible = o.edgeVisible !== false; mesh.visible = o.edgeVisible !== false;
group.add(mesh); group.add(mesh);
}); });
+44 -2
View File
@@ -157,9 +157,18 @@ window.UnitModel = (() => {
const full = s.axis === 'x' ? W / 2 : D / 2; const full = s.axis === 'x' ? W / 2 : D / 2;
const ext0 = Math.max(0, Math.round(s0 + full)), ext1 = Math.max(0, Math.round(full - s1)); const ext0 = Math.max(0, Math.round(s0 + full)), ext1 = Math.max(0, Math.round(full - s1));
const skinExt = s.dir > 0 ? { left: ext0, right: ext1 } : { left: ext1, right: ext0 }; const skinExt = s.dir > 0 ? { left: ext0, right: ext1 } : { left: ext1, right: ext0 };
// Doorlopende wand die op een hoek vervalt: de haakse wand loopt daar
// tot de vloerrand door en staat dus op deze wandlijn (endInset, u-mm
// vanaf links/rechts). Een opbouw in die naad-zone begint erna.
const through = state.layout.corners === 'zijkanten' ? s.axis === 'z' : s.axis === 'x';
const pL = s.dir > 0 ? s0 : s1, pR = s.dir > 0 ? s1 : s0;
const endInset = {
left: through && cutAt(s.key, pL) ? T[WALL_ENDS[s.key].left] : 0,
right: through && cutAt(s.key, pR) ? T[WALL_ENDS[s.key].right] : 0
};
return { return {
side: s.key, label: s.label, nr: s.nr, rotY: s.rotY, normal: s.normal, axis: s.axis, side: s.key, label: s.label, nr: s.nr, rotY: s.rotY, normal: s.normal, axis: s.axis,
thickness: T[s.key], width, cx, cz, cutsU, pieces, skinExt thickness: T[s.key], width, cx, cz, cutsU, pieces, skinExt, endInset
}; };
}); });
@@ -170,7 +179,13 @@ window.UnitModel = (() => {
// op de volle zijde opgelost (ankers gelden voor de hele zijde) en daarna // op de volle zijde opgelost (ankers gelden voor de hele zijde) en daarna
// aan het stuk toegewezen waar ze volledig in vallen; openingen die in een // aan het stuk toegewezen waar ze volledig in vallen; openingen die in een
// naad vallen vervallen (dropped). // naad vallen vervallen (dropped).
function wallPieces(wallLayout, wallState) { // raised (optioneel): [{ u0, u1, base }] = opbouw in een naad-zone: daar
// staat de wand pas vanaf hoogte `base` (mm vanaf het loopvlak, bijv. boven
// het lagere dak van de buur-unit) tot de daklijn. Zo'n stuk heeft
// baseY = base; openingen die er volledig boven base in vallen horen erbij
// (y t.o.v. de onderkant van het stuk). Te lage opbouw vervalt.
const MIN_RAISE_MM = 150;
function wallPieces(wallLayout, wallState, raised) {
const fullConfig = WallModel.configFromState(wallState); const fullConfig = WallModel.configFromState(wallState);
const W = wallLayout.width; const W = wallLayout.width;
const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight; const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight;
@@ -200,6 +215,33 @@ window.UnitModel = (() => {
config, frame: WallModel.computeFrame(config) config, frame: WallModel.computeFrame(config)
}; };
}); });
const raisedList = (raised || []).filter(r => r.u1 - r.u0 >= MIN_PIECE_MM);
const ins = wallLayout.endInset || { left: 0, right: 0 };
raisedList.forEach((r, i) => {
const { u0: a, u1: b, base } = r;
const hasPeak = hM !== null && a < pU - 1 && b > pU + 1;
const hA = heightAt(a) - base, hB = heightAt(b) - base;
if (Math.min(hA, hB) < MIN_RAISE_MM) return;
const openings = fullConfig.openings
.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 }; });
const params = Object.assign({}, wallState.wall, {
width: b - a, heightLeft: Math.round(hA), heightRight: Math.round(hB),
heightMid: hasPeak ? hM - base : null, peakX: hasPeak ? pU - a : undefined
});
const config = WallModel.buildConfig(params, openings);
pieces.push({
suffix: 'o' + (raisedList.length > 1 ? i + 1 : ''), // opbouw
u0: a, u1: b, lxCenter: -W / 2 + (a + b) / 2, baseY: Math.round(base),
// Naast de haakse wand in de hoek (endInset) bedekken de buitenlagen
// ook diens kopse kant, die boven het lage dak bloot ligt.
skinExt: {
left: ins.left > 0 && a <= ins.left + EPS ? ins.left : 0,
right: ins.right > 0 && b >= W - ins.right - EPS ? ins.right : 0
},
config, frame: WallModel.computeFrame(config)
});
});
const dropped = fullConfig.openings.filter(op => !used.has(op.id)); const dropped = fullConfig.openings.filter(op => !used.has(op.id));
return { pieces, dropped, fullConfig }; return { pieces, dropped, fullConfig };
} }
+15 -4
View File
@@ -508,6 +508,9 @@ window.WallModel = (() => {
// linkerrand van dit stuk. Het verticale latten-raster volgt dan de // linkerrand van dit stuk. Het verticale latten-raster volgt dan de
// hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt // hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt
// alleen aan het einde van de gevel. // alleen aan het einde van de gevel.
// facade.gridY0 (optioneel, mm, wand-coördinaten): hoogte waarop het
// horizontale latten-raster van de hele gevel begint. Een wandstuk dat
// hoger begint (opbouw, blootliggende naadwand) volgt zo hetzelfde raster.
// skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het // skinFrom (optioneel, mm): buitenafwerking pas vanaf deze hoogte (het
// blootliggende deel van een naadwand boven het dak van de andere unit). // blootliggende deel van een naadwand boven het dak van de andere unit).
function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) { function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) {
@@ -524,7 +527,12 @@ window.WallModel = (() => {
// Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat). // Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat).
const horizontaal = []; const horizontaal = [];
for (let y = y0 + w / 2; y < tl.maxY - w / 2; y += sp) { let yStart = y0 + w / 2;
if (facade && facade.gridY0 !== undefined) {
const g = facade.gridY0 + w / 2;
yStart = g + Math.max(0, Math.ceil((yStart - g) / sp - 1e-9)) * sp;
}
for (let y = yStart; y < tl.maxY - w / 2; y += sp) {
skinSegmentsAtY(CONFIG, y).forEach(([x0, x1]) => { skinSegmentsAtY(CONFIG, y).forEach(([x0, x1]) => {
if (x0 <= -halfW + 0.5) x0 = xL; if (x0 <= -halfW + 0.5) x0 = xL;
if (x1 >= halfW - 0.5) x1 = xR; if (x1 >= halfW - 0.5) x1 = xR;
@@ -646,10 +654,13 @@ window.WallModel = (() => {
// Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over // Watermerk-UV's (doek) in gevel-coördinaten, zodat het patroon over
// naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving). // naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving).
const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0; const uvShiftM = facade ? (CONFIG.wallWidth / 2 + facade.u0) * 0.001 : 0;
// Verticaal: een hoger beginnend stuk (facade.yOff, mm boven het
// loopvlak van de gevel) schuift het watermerk mee omhoog.
const uvShiftYM = facade && facade.yOff ? facade.yOff * 0.001 : 0;
const shiftUV = (geo) => { const shiftUV = (geo) => {
if (!uvShiftM) return geo; if (!uvShiftM && !uvShiftYM) return geo;
const uv = geo.attributes.uv; const uv = geo.attributes.uv;
for (let k = 0; k < uv.count; k++) uv.setX(k, uv.getX(k) + uvShiftM); for (let k = 0; k < uv.count; k++) { uv.setX(k, uv.getX(k) + uvShiftM); uv.setY(k, uv.getY(k) + uvShiftYM); }
return geo; return geo;
}; };
@@ -1335,7 +1346,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, trasraamFor, 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, trasraamFor, skinStartY, computeBattens, planPurchase, purchaseText,
sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines, sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines,
validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame, validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame,
createMaterials, build3D, computeSpecs createMaterials, build3D, computeSpecs
+3 -2
View File
@@ -1028,10 +1028,11 @@ function mountWallEditor(container, callbacks) {
} }
// Naad-zones (wand vervalt daar, zie woning) als gearceerde overlay. // Naad-zones (wand vervalt daar, zie woning) als gearceerde overlay.
// Met upTo (mm): opbouw boven het lagere dak, alleen gearceerd tot daar.
function drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM) { function drawSeamOverlay(toCanvasX, toCanvasY, halfWMM, maxRoofMM) {
seamCuts.forEach(([u0, u1]) => { seamCuts.forEach(([u0, u1, upTo]) => {
const x0 = toCanvasX(-halfWMM + u0), x1 = toCanvasX(-halfWMM + u1); const x0 = toCanvasX(-halfWMM + u0), x1 = toCanvasX(-halfWMM + u1);
const yTop = toCanvasY(maxRoofMM), yBot = toCanvasY(0); const yTop = toCanvasY(upTo > 0 ? Math.min(upTo, maxRoofMM) : maxRoofMM), yBot = toCanvasY(0);
ctxFront.save(); ctxFront.save();
ctxFront.fillStyle = 'rgba(17, 17, 17, 0.8)'; ctxFront.fillStyle = 'rgba(17, 17, 17, 0.8)';
ctxFront.fillRect(x0, yTop, x1 - x0, yBot - yTop); ctxFront.fillRect(x0, yTop, x1 - x0, yBot - yTop);