diff --git a/README.md b/README.md index 4ad1ccc..92b3b3d 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,12 @@ assets/ logo, texturen, skybox - **Woning:** units koppelen aan een zijde (uitlijning + verschuiving); 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 - A of B. De hoogte volgt per deel de daklijn van zijn unit; steekt een deel - boven het dak van de andere unit uit, dan krijgt dat stuk buitenafwerking - (vochtdoek + latwerk) die met de gevel meeloopt. + A of B. De hoogte volgt per deel de daklijn van zijn unit. Staat een unit + met een hogere daklijn tegen een lager dak, dan krijgt zijn buitenwand aan + 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. - **Dak:** per unit opgebouwd als gespiegelde vloer + dakleer, in de vorm 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. Is een dak langs de naad minstens een boeideelhoogte hoger dan het 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; sandwichpanelen rondom, 600 mm de grond in. - **Verlichting:** boeideel-spots of gevellampen (per gevel verdeeld, diff --git a/js/app.js b/js/app.js index 84d0422..e9cac08 100644 --- a/js/app.js +++ b/js/app.js @@ -4,7 +4,7 @@ * Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming. * 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. let tabs = []; diff --git a/js/house/house-model.js b/js/house/house-model.js index c374c44..d5bc2ca 100644 --- a/js/house/house-model.js +++ b/js/house/house-model.js @@ -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)); diff --git a/js/lighting/lighting-model.js b/js/lighting/lighting-model.js index eca8629..9717ee3 100644 --- a/js/lighting/lighting-model.js +++ b/js/lighting/lighting-model.js @@ -119,6 +119,7 @@ window.LightingModel = (() => { const segs = []; units.forEach(u => u.walls.forEach(w => w.pieces.forEach(p => { p.lamps = []; + if (p.baseY) return; // opbouw boven een lager dak: geen gevellampen const W = p.place.width; // Lampen hangen op de afgewerkte gevel: wandvlak + kruislat + bekleding. const T = p.place.thickness + 2 * WallModel.claddingOffset(p.buitenMateriaal); diff --git a/js/roof/roof-model.js b/js/roof/roof-model.js index 60fd4d6..6d00bc8 100644 --- a/js/roof/roof-model.js +++ b/js/roof/roof-model.js @@ -28,7 +28,9 @@ // dakrand = standaard de buitenkant gevel), standaard 300 mm hoog. // Spots in de onderplaat staan op 40% van de overstek (spotLine); welke // 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. // ============================================================ @@ -37,7 +39,7 @@ window.RoofModel = (() => { const DAKLEER_MM = 2; // visuele dikte van het dakleer const DENSITY_DAKLEER_KGM2 = 1.4; // EPDM ~1,4 kg/m2 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 OVERHANG_MIN = 200, OVERHANG_MAX = 400; @@ -357,17 +359,35 @@ window.RoofModel = (() => { return { a, b, owner: owner.unitId, fascia, normal: rectilinearNormal(a, b), 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 }; }); - // 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) => { + if (sg.dropped) return; 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 square = (key, v) => { sg[key] = alongZ ? { x: sg[key].x, z: v.z } : { x: v.x, z: sg[key].z }; }; - if (!sg.dropped && prev.dropped) square('outerA', sg.a); - if (!sg.dropped && next.dropped) square('outerB', sg.b); + const at = (q, v) => alongZ ? { x: q.x, z: v.z } : { x: v.x, z: q.z }; + [['a', 'outerA', 'spotA', 'b', prev], ['b', 'outerB', 'spotB', 'a', next]].forEach(([k, outerK, spotK, otherK, nb]) => { + 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)); }); @@ -377,7 +397,7 @@ window.RoofModel = (() => { return { segments, overhang: s.overhang, spotRatio: SPOT_RATIO, 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), 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, @@ -431,10 +451,11 @@ window.RoofModel = (() => { 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); }); - const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight; - const topFn = sg.fascia ? sg.topAt : (p) => sg.topAt(p) + edge.trimAbove; + // Boeideel: totale hoogte fasciaHeight vanaf de bovenkant van de rand; + // daktrim: trimHeight onder het dakleer + de opstand erboven. + const h = sg.fascia ? edge.fasciaHeight : edge.trimHeight + edge.edgeAbove; 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; group.add(mesh); }); diff --git a/js/unit/unit-model.js b/js/unit/unit-model.js index 4f1ca0d..b0be2c7 100644 --- a/js/unit/unit-model.js +++ b/js/unit/unit-model.js @@ -157,9 +157,18 @@ window.UnitModel = (() => { 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 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 { 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 // aan het stuk toegewezen waar ze volledig in vallen; openingen die in een // 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 W = wallLayout.width; const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight; @@ -200,6 +215,33 @@ window.UnitModel = (() => { 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)); return { pieces, dropped, fullConfig }; } diff --git a/js/wall/wall-model.js b/js/wall/wall-model.js index 16feb3b..8935f35 100644 --- a/js/wall/wall-model.js +++ b/js/wall/wall-model.js @@ -508,6 +508,9 @@ window.WallModel = (() => { // linkerrand van dit stuk. Het verticale latten-raster volgt dan de // hele gevel i.p.v. per stuk opnieuw te beginnen; een eindlat komt // 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 // blootliggende deel van een naadwand boven het dak van de andere unit). function computeBattens(CONFIG, cladding, trasraam, skinExt, facade, skinFrom) { @@ -524,7 +527,12 @@ window.WallModel = (() => { // Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat). 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]) => { if (x0 <= -halfW + 0.5) x0 = xL; 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 // naden/wandstukken doorloopt (UV = vormcoördinaat in m + verschuiving). 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) => { - if (!uvShiftM) return geo; + if (!uvShiftM && !uvShiftYM) return geo; 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; }; @@ -1335,7 +1346,7 @@ window.WallModel = (() => { } 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, validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame, createMaterials, build3D, computeSpecs diff --git a/js/wall/wall.js b/js/wall/wall.js index 11f069e..a98a8e7 100644 --- a/js/wall/wall.js +++ b/js/wall/wall.js @@ -1028,10 +1028,11 @@ function mountWallEditor(container, callbacks) { } // 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) { - seamCuts.forEach(([u0, u1]) => { + seamCuts.forEach(([u0, u1, upTo]) => { 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.fillStyle = 'rgba(17, 17, 17, 0.8)'; ctxFront.fillRect(x0, yTop, x1 - x0, yBot - yTop);