From 9126588df7c151038433486c7c93ddb6f1136177 Mon Sep 17 00:00:00 2001 From: Machiel Date: Wed, 30 Sep 2026 13:35:21 +0200 Subject: [PATCH] v0.4.10: wandhoogtes gekoppeld, middenpunt (nok), binnenspots langs naadwanden - WallModel: optioneel middenpunt (heightMid, topLine), bovenregels in verstek; stijlen, openingen, beplating, doek, latwerk, BOM, 2D en PDF volgen de knik - wand-editor: schakelaar "Middenpunt (nok)" + nokhoogte, links/rechts = goothoogte - UnitModel.syncWallHeights: tegenoverliggende wand gespiegeld, haakse wanden vlak op de hoekhoogtes; wallPieces met nok per stuk - LightingModel.computeIndoor: raster vanaf binnenzijde eigen naadwanddeel Co-Authored-By: Claude Opus 5.5 --- README.md | 11 ++ js/app.js | 2 +- js/house/house-model.js | 2 +- js/house/house.js | 10 +- js/lighting/lighting-model.js | 14 +- js/unit/unit-model.js | 47 ++++++- js/wall/wall-model.js | 238 ++++++++++++++++++++++------------ js/wall/wall-pdf.js | 39 +++--- js/wall/wall.js | 67 +++++++--- 9 files changed, 295 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index dac15b3..6cb8765 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,17 @@ assets/ logo (preloader) + bestemmingsmap voor toekomstige 'geen'); de openingen gelden voor beide delen en worden gespiegeld. Vloer en dak krijgen op een naad langs voor-/achterrand een kopbalk (FloorModel.computeFrame rims), langs de zijranden ligt al een balk. +- **Wandhoogtes gekoppeld + nok (v0.4.10).** Een wand kan een middenpunt + (nok, wall.heightMid) krijgen: de bovenrand wordt dan twee stukken, links/ + rechts zijn goothoogtes (WallModel.topLine; bovenregels in verstek). In een + unit bepaalt de laatst in hoogte gewijzigde wand de dakvorm + (UnitModel.syncWallHeights): de tegenoverliggende wand krijgt dezelfde + hoogtes gespiegeld (incl. nok), de haakse wanden worden vlak op de + hoekhoogtes. Binnen- en naadwanden doen (nog) niet mee; de daklogica volgt + apart. +- **Binnenspots langs naadwanden (v0.4.10).** Het raster begint vanaf de + binnenzijde van het eigen naadwanddeel (niet de unitrand), zodat de rij + langs de naad niet meer wegvalt. - **Isolatie niet meer getekend (v0.4.9).** Filter "Isolatie" en de 3D-lagen zijn weg; de spouw-/leegte-inhoud blijft in specificaties en totalen. - **Startpunt gevellampen (v0.4.8).** Een gevel telt vanaf de zichtbare, diff --git a/js/app.js b/js/app.js index eb58160..513831e 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.9'; +const APP_VERSION = '0.4.10'; // 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 2b2c870..160f6ac 100644 --- a/js/house/house-model.js +++ b/js/house/house-model.js @@ -376,7 +376,7 @@ window.HouseModel = (() => { // wandbovenkant van deze unit. const roofData = FloorModel.computeFromState(u.roof.deck, rims); const roofSpecs = RoofModel.computeSpecs(roofData.calcData, roofData.resolvedOpenings); - const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => Math.max(ws.wall.heightLeft, ws.wall.heightRight))); + const wallTop = Math.max(...Object.values(u.unit.walls).map(ws => UnitModel.wallTopOf(ws))); const roofW = roofData.calcData.width, roofD = roofData.calcData.depth; const roofCx = rect.cx + (u.roof.offsetX || 0), roofCz = rect.cz + (u.roof.offsetZ || 0); const roof = { diff --git a/js/house/house.js b/js/house/house.js index 8ea8c7e..85112b9 100644 --- a/js/house/house.js +++ b/js/house/house.js @@ -133,7 +133,15 @@ function mountHouseEditor(container, callbacks) { if (!activeEditor || typeof activeEditor.getStateJson !== 'function' || !activePart) return; const [kind, a, b] = activePart.split(':'); if (kind === 'floor' && unitById(a)) unitById(a).unit.floor = activeEditor.getStateJson(); - else if (kind === 'wall' && unitById(a)) unitById(a).unit.walls[b] = activeEditor.getStateJson(); + else if (kind === 'wall' && unitById(a)) { + // Hoogtes gewijzigd: tegenoverliggende wand spiegelen, haakse wanden + // vlak op de hoekhoogtes (UnitModel.syncWallHeights). + const unit = unitById(a).unit, before = unit.walls[b].wall; + const next = activeEditor.getStateJson(); + unit.walls[b] = next; + const h = (w) => [w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : null].join('|'); + if (h(before) !== h(next.wall)) UnitModel.syncWallHeights(unit, b); + } else if (kind === 'roof' && unitById(a)) unitById(a).roof.deck = activeEditor.getStateJson(); else if (kind === 'iw' && iwById(Number(a))) { const iw = iwById(Number(a)); diff --git a/js/lighting/lighting-model.js b/js/lighting/lighting-model.js index fd1d9e8..5b99dd5 100644 --- a/js/lighting/lighting-model.js +++ b/js/lighting/lighting-model.js @@ -256,13 +256,23 @@ window.LightingModel = (() => { const T = {}; u.walls.forEach(w => { T[w.side] = w.removed ? 0 : w.thickness; }); const r = u.rect, off = s.indoorOffset; + // Naadwanddelen van deze unit staan binnen de rect tegen de naad: het + // raster begint dan vanaf hun binnenzijde (net als bij een buitenwand). + const own = interior.filter(iw => !iw.seamUnit || iw.seamUnit === u.id); + own.filter(iw => iw.seamUnit).forEach(iw => { + const p = iw.place, horiz = Math.abs(Math.sin(p.rotY)) < 0.5; + const [side, d] = horiz + ? (Math.abs(p.cz - r.z0) < Math.abs(p.cz - r.z1) ? ['voor', p.cz - r.z0] : ['achter', r.z1 - p.cz]) + : (Math.abs(p.cx - r.x0) < Math.abs(p.cx - r.x1) ? ['links', p.cx - r.x0] : ['rechts', r.x1 - p.cx]); + T[side] = Math.max(T[side] || 0, d + p.thickness / 2); + }); const xs = gridPositions(r.x0 + (T.links || 0) + off, r.x1 - (T.rechts || 0) - off, s.indoorMin, s.indoorMax); const zs = gridPositions(r.z0 + (T.voor || 0) + off, r.z1 - (T.achter || 0) - off, s.indoorMin, s.indoorMax); xs.forEach(x => zs.forEach(z => { - const tooClose = interior.some(iw => { + const tooClose = own.some(iw => { const half = iw.place.width / 2, c = Math.cos(iw.place.rotY), sn = Math.sin(iw.place.rotY); const a = { x: iw.place.cx - half * c, z: iw.place.cz + half * sn }, b = { x: iw.place.cx + half * c, z: iw.place.cz - half * sn }; - return distToSegment({ x, z }, a, b) < iw.place.thickness / 2 + off; + return distToSegment({ x, z }, a, b) < iw.place.thickness / 2 + off - 1; }); if (!tooClose) out.push({ unitId: u.id, x, z, y: u.roof.base, floorY: u.layout.floorTop }); })); diff --git a/js/unit/unit-model.js b/js/unit/unit-model.js index dac2914..4f1ca0d 100644 --- a/js/unit/unit-model.js +++ b/js/unit/unit-model.js @@ -174,7 +174,12 @@ window.UnitModel = (() => { const fullConfig = WallModel.configFromState(wallState); const W = wallLayout.width; const hL = wallState.wall.heightLeft, hR = wallState.wall.heightRight; - const heightAt = (u) => hL + (u / W) * (hR - hL); + // Middenpunt (nok) midden op de wand; een stuk krijgt de nok alleen als + // die erin valt, anders de daklijn ter plaatse als gewone helling. + const hM = wallState.wall.heightMid > 0 ? Math.max(wallState.wall.heightMid, hL, hR) : null; + const pU = W / 2; + const heightAt = (u) => hM === null ? hL + (u / W) * (hR - hL) + : u <= pU ? hL + (u / pU) * (hM - hL) : hM + ((u - pU) / (W - pU)) * (hR - hM); const multi = wallLayout.pieces.length > 1; const used = new Set(); @@ -182,8 +187,10 @@ window.UnitModel = (() => { const openings = fullConfig.openings .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 }; }); + const hasPeak = hM !== null && a < pU - 1 && b > pU + 1; 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)), + heightMid: hasPeak ? hM : null, peakX: hasPeak ? pU - a : undefined }); const config = WallModel.buildConfig(params, openings); return { @@ -197,6 +204,40 @@ window.UnitModel = (() => { return { pieces, dropped, fullConfig }; } + // Wandhoogtes koppelen, zodat er altijd een logisch dak op past. De + // bewerkte wand (side) bepaalt de dakvorm: + // - de tegenoverliggende wand krijgt dezelfde hoogtes gespiegeld (van + // buitenaf gezien wisselen links en rechts), incl. middenpunt (nok); + // - de twee haakse wanden worden vlak op de hoogte van de hoek waar ze + // op aansluiten (goothoogte bij een lessenaar- of zadeldak). + // Voorbeeld: west zuid hoger dan noord -> oost ook, noordwand vlak op de + // lage, zuidwand vlak op de hoge hoogte. Muteert state. + // Welke haakse wand zit aan de linker/rechterkant (van buitenaf gezien). + const WALL_ENDS = { + voor: { left: 'rechts', right: 'links' }, + achter: { left: 'links', right: 'rechts' }, + links: { left: 'voor', right: 'achter' }, + rechts: { left: 'achter', right: 'voor' } + }; + const OPPOSITE = { voor: 'achter', achter: 'voor', links: 'rechts', rechts: 'links' }; + function syncWallHeights(state, side) { + const w = state.walls[side].wall; + const mid = w.heightMid > 0 ? w.heightMid : null; + const set = (key, hL, hR, hM) => { + const t = state.walls[key].wall; + t.heightLeft = hL; t.heightRight = hR; t.heightMid = hM; + t.heightAll = hM === null && hL === hR ? hL : w.heightAll; + }; + set(OPPOSITE[side], w.heightRight, w.heightLeft, mid); + set(WALL_ENDS[side].left, w.heightLeft, w.heightLeft, null); + set(WALL_ENDS[side].right, w.heightRight, w.heightRight, null); + } + // Hoogste punt van een wand (incl. nok). + function wallTopOf(wallState) { + const w = wallState.wall; + return Math.max(w.heightLeft, w.heightRight, w.heightMid > 0 ? w.heightMid : 0); + } + function validateState(design) { if (!design.floor) throw new Error('veld "floor" ontbreekt'); try { FloorModel.validateState(design.floor); } catch (err) { throw new Error('vloer: ' + err.message); } @@ -210,5 +251,5 @@ window.UnitModel = (() => { } } - return { WALL_SIDES, CORNER_MODES, clone, createDefaultState, localToWorld, computeLayout, wallPieces, validateState }; + return { WALL_SIDES, CORNER_MODES, clone, createDefaultState, localToWorld, computeLayout, wallPieces, syncWallHeights, wallTopOf, validateState }; })(); diff --git a/js/wall/wall-model.js b/js/wall/wall-model.js index 4be1def..9ef63fb 100644 --- a/js/wall/wall-model.js +++ b/js/wall/wall-model.js @@ -14,7 +14,7 @@ // voor zowel de losse wand als de unit gelden. // // Werkt met een "wand-state" (zelfde formaat als getStateJson/JSON-export): -// { wall: {width, heightAll, heightLeft, heightRight, studWidth, ...}, +// { wall: {width, heightAll, heightLeft, heightRight, heightMid?, studWidth, ...}, // materials: {interior, exterior}, openings: [{id,w,h,y,anchor,offset}] } // // Plain script (geen ES-module), moet na core/geometry.js geladen worden. @@ -102,6 +102,81 @@ window.WallModel = (() => { // deel van de andere unit aan staat (HouseModel, naadwanden). const EXTERIOR_MATERIALS = { vochtdoek: 'Vochtdoek', cementplaat: 'Cement-gebonden platen', spaanplaat: 'Spaanplaat', geen: 'Geen (naadzijde)' }; + // Bovenrand van de wand (daklijn): één recht stuk van links naar rechts, + // of met een middenpunt (nok, wallPeak) twee stukken die elkaar daar + // raken. De nok ligt nooit lager dan de goten (buildConfig), dus de + // bovenrand is overal het MINIMUM van de (doorgetrokken) stukken — dat + // houdt at/under/rangeAbove simpel en maakt "laagste punt boven een + // opening" gelijk aan het laagste van de twee uiteinden. + // x t.o.v. het wandmidden, alles in mm x k (k = 0.001: meters). + // at(x) bovenkant (daklijn) + // under(x) onderkant bovenregel(s); knee = hun snijpunt (verstek) + // rangeAbove(y) [x0, x1] waar de daklijn boven y ligt (of null) + // crossings(y) x-posities waar de daklijn y kruist (knikpunten) + // underPts(a, b) onderkant bovenregel van a naar b, incl. knie + function topLine(CONFIG, k) { + k = k || 1; + const halfW = CONFIG.wallWidth / 2 * k; + const L = { x: -halfW, y: CONFIG.wallHeightLeft * k }, R = { x: halfW, y: CONFIG.wallHeightRight * k }; + const pk = CONFIG.wallPeak ? { x: (CONFIG.wallPeak.x - CONFIG.wallWidth / 2) * k, y: CONFIG.wallPeak.h * k } : null; + const pts = pk ? [L, pk, R] : [L, R]; + const segs = []; + for (let i = 0; i < pts.length - 1; i++) { + const a = pts[i], b = pts[i + 1], slope = (b.y - a.y) / (b.x - a.x); + segs.push({ a, b, slope, vo: CONFIG.plateThick * k * Math.sqrt(1 + slope * slope) }); + } + const lineY = (sg, x) => sg.a.y + (x - sg.a.x) * sg.slope; + const at = (x) => Math.min(...segs.map(sg => lineY(sg, x))); + const under = (x) => Math.min(...segs.map(sg => lineY(sg, x) - sg.vo)); + let knee = null; + if (segs.length === 2) { + const [s0, s1] = segs, ds = s0.slope - s1.slope; + // s0.a.y + (x - s0.a.x) s0.slope - vo0 = s1.a.y + (x - s1.a.x) s1.slope - vo1 + const x = Math.abs(ds) > 1e-12 + ? ((s1.a.y - s1.a.x * s1.slope - s1.vo) - (s0.a.y - s0.a.x * s0.slope - s0.vo)) / ds + : pk.x; + knee = { x, y: under(x) }; + } + const rangeAbove = (y) => { + let x0 = -halfW, x1 = halfW; + segs.forEach(sg => { + if (Math.abs(sg.slope) < 1e-12) { if (sg.a.y <= y) x1 = x0 - 1; return; } + const xc = sg.a.x + (y - sg.a.y) / sg.slope; + if (sg.slope > 0) x0 = Math.max(x0, xc); else x1 = Math.min(x1, xc); + }); + return x1 > x0 ? [x0, x1] : null; + }; + const crossings = (y) => segs.filter(sg => Math.abs(sg.slope) > 1e-12) + .map(sg => ({ sg, x: sg.a.x + (y - sg.a.y) / sg.slope })) + .filter(c => c.x > c.sg.a.x && c.x < c.sg.b.x).map(c => c.x); + const underPts = (xa, xb) => { + const out = [{ x: xa, y: under(xa) }]; + if (knee && knee.x > xa && knee.x < xb) out.push({ x: knee.x, y: knee.y }); + out.push({ x: xb, y: under(xb) }); + return out; + }; + return { + pts, segs, peak: pk, knee, at, under, rangeAbove, crossings, underPts, + maxY: Math.max(...pts.map(p => p.y)), minY: Math.min(L.y, R.y), + kinks: pk ? [pk.x] : [] + }; + } + + // Omtrek(ken) van de bovenregel(s) (zelfde eenheid als topLine): één + // parallellogram, of met nok twee stukken in verstek (nok boven, knie onder). + function topPlateOutlines(tl) { + const [L, R] = [tl.pts[0], tl.pts[tl.pts.length - 1]]; + if (!tl.peak) { + const vo = tl.segs[0].vo; + return [[L, R, { x: R.x, y: R.y - vo }, { x: L.x, y: L.y - vo }]]; + } + const P = tl.peak, K = tl.knee; + return [ + [L, P, K, { x: L.x, y: L.y - tl.segs[0].vo }], + [P, R, { x: R.x, y: R.y - tl.segs[1].vo }, K] + ]; + } + // Heuristiek (geen volwaardige constraint-solver): elke opening wordt eerst // individueel gepositioneerd op basis van zijn anker, dan van links naar // rechts gesorteerd en tegen elkaar aan geduwd als er overlap is, en tot @@ -116,7 +191,8 @@ window.WallModel = (() => { // het laagste punt van de hele wand. Bij een hellende wand mag een opening // dicht bij de hoge kant dus hoger/groter zijn dan een opening dicht bij // de lage kant. - function resolveOpenings(rawOpenings, wallWidth, studWidth, wallHeightLeft, wallHeightRight, plateThick) { + // roofAt(xLocal): daklijn, xLocal vanaf de linker wandrand (0..wallWidth). + function resolveOpenings(rawOpenings, wallWidth, studWidth, roofAt, plateThick) { // 2850mm is de vaste referentie-bovengrens (bijv. max. sparingbreedte in // dit constructiesysteem); daarnaast blijft de wand zelf ook een harde // grens (er moet nog stijlruimte overblijven). @@ -179,8 +255,9 @@ window.WallModel = (() => { // die opening — het punt waar een verticale lijn op die plek de // (eventueel hellende) horizontale topbalk zou snijden. Bij een // opening die zelf een breedte heeft, wordt het laagste van de twee - // uiteinden aangehouden (de daklijn is lineair, dus dat is voldoende). - const localRoofHeightAt = (xLocal) => wallHeightLeft + (xLocal / wallWidth) * (wallHeightRight - wallHeightLeft); + // uiteinden aangehouden (de daklijn is lineair of heeft een nok als + // hoogste punt, dus dat is voldoende). + const localRoofHeightAt = roofAt; resolved.forEach(o => { const localCeiling = Math.min(localRoofHeightAt(o.x), localRoofHeightAt(o.x + o.w)); @@ -216,11 +293,24 @@ window.WallModel = (() => { const hRight = Math.round(params.heightRight || 2900); const studWidth = Math.round(params.studWidth || STANDARD_BEAM.width); const plateThick = Math.round(params.plateThick || STANDARD_BEAM.width); + // Middenpunt (nok): standaard midden op de wand, of op peakX (mm vanaf + // links; een wandstuk uit een unit waar de nok niet in het midden + // valt). Nooit lager dan de hoogste goot; gelijk = geen nok. + let wallPeak = null; + if (params.heightMid > 0) { + const px = params.peakX !== undefined ? params.peakX : wallWidth / 2; + const h = Math.round(Math.max(params.heightMid, hLeft, hRight)); + if (px > 1 && px < wallWidth - 1 && h > hLeft + (px / wallWidth) * (hRight - hLeft) + 0.5) wallPeak = { x: px, h }; + } + const roofAt = (x) => wallPeak + ? (x <= wallPeak.x ? hLeft + x / wallPeak.x * (wallPeak.h - hLeft) : wallPeak.h + (x - wallPeak.x) / (wallWidth - wallPeak.x) * (hRight - wallPeak.h)) + : hLeft + (x / wallWidth) * (hRight - hLeft); return { wallWidth, wallHeightLeft: hLeft, wallHeightRight: hRight, + wallPeak, studWidth, studDepth: Math.round(params.studDepth || STANDARD_BEAM.depth), plateThick, @@ -228,7 +318,7 @@ window.WallModel = (() => { sheathingThick: Math.round(params.sheathingThick || 10), minSpacing: Math.round(params.minSpacing || 240), maxSpacing: Math.round(params.maxSpacing || 480), - openings: resolveOpenings(rawOpenings, wallWidth, studWidth, hLeft, hRight, plateThick) + openings: resolveOpenings(rawOpenings, wallWidth, studWidth, roofAt, plateThick) }; } @@ -247,8 +337,12 @@ window.WallModel = (() => { // Regelwerk: volledige stijlen (incl. koningsstijlen) + kreupele stijlen. function computeFrame(CONFIG) { const startX = -CONFIG.wallWidth / 2; - const slope = (CONFIG.wallHeightRight - CONFIG.wallHeightLeft) / CONFIG.wallWidth; - const angleDeg = Math.round(Math.atan(slope) * (180 / Math.PI)); + const top = topLine(CONFIG); + const slope = top.segs[0].slope; + // Met nok: de steilste helling (beide kanten lopen dan op naar de nok). + const angleDeg = top.peak + ? Math.round(Math.max(...top.segs.map(sg => Math.atan(Math.abs(sg.slope)))) * (180 / Math.PI)) + : Math.round(Math.atan(slope) * (180 / Math.PI)); // Dubbele hoekstijlen: de buitenste staat op de wandrand, de tweede er // direct tegenaan. De regelmatige verdeling begint bij de binnenste. @@ -319,8 +413,7 @@ window.WallModel = (() => { // Gebruikt door build3D (fill) én de 2D-tekening (naad-/baanlijnen). function sheathingHoles(CONFIG) { const halfW = CONFIG.wallWidth / 2; - const slope = (CONFIG.wallHeightRight - CONFIG.wallHeightLeft) / CONFIG.wallWidth; - const topYAt = (x) => CONFIG.wallHeightLeft + (x + halfW) * slope; + const topYAt = topLine(CONFIG).at; const roofSafetyMargin = 10; return CONFIG.openings.map(op => { const hx0 = -halfW + op.x - CONFIG.studWidth; @@ -336,16 +429,11 @@ window.WallModel = (() => { // liggen: binnen de wand-omtrek (incl. schuine top) en buiten de // uitsparingen. Geeft [[x0, x1], ...] terug (mm, t.o.v. het wandmidden). function skinSegmentsAtY(CONFIG, y) { - const halfW = CONFIG.wallWidth / 2; - const hL = CONFIG.wallHeightLeft, hR = CONFIG.wallHeightRight; - if (y <= 0 || y >= Math.max(hL, hR)) return []; + if (y <= 0) return []; // Deel van de breedte waar de daklijn boven y ligt. - let x0 = -halfW, x1 = halfW; - if (hL !== hR) { - const xAtY = -halfW + (y - hL) / (hR - hL) * CONFIG.wallWidth; - if (hR > hL) x0 = Math.max(x0, xAtY); else x1 = Math.min(x1, xAtY); - } else if (y >= hL) return []; - let segments = [[x0, x1]]; + const range = topLine(CONFIG).rangeAbove(y); + if (!range) return []; + let segments = [range]; sheathingHoles(CONFIG).filter(h => y > h.hy0 && y < h.hy1).forEach(h => { segments = segments.flatMap(([a, b]) => { if (h.hx1 <= a || h.hx0 >= b) return [[a, b]]; @@ -376,15 +464,14 @@ window.WallModel = (() => { const gridX0 = facade ? -CONFIG.wallWidth / 2 - facade.u0 - (facade.extStart ? 2 * BATTEN.thick : 0) : xL; const isEnd = facade ? facade.isEnd : true; const halfW = CONFIG.wallWidth / 2; - const hL = CONFIG.wallHeightLeft, hR = CONFIG.wallHeightRight; - const topYAt = (x) => hL + (x + halfW) * (hR - hL) / CONFIG.wallWidth; + const tl = topLine(CONFIG), topYAt = tl.at; const holes = sheathingHoles(CONFIG); const w = BATTEN.width, sp = BATTEN.spacing; const top = (cladding === 'verticaal') ? 'horizontaal' : 'verticaal'; // Horizontale latten: vanaf de onderkant elke 400 mm (hartmaat). const horizontaal = []; - for (let y = y0 + w / 2; y < Math.max(hL, hR) - w / 2; y += sp) { + for (let y = y0 + w / 2; 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; @@ -551,10 +638,11 @@ window.WallModel = (() => { const hRightM = CONFIG.wallHeightRight * toM; const halfW = wWidthM / 2; - const slope = (hRightM - hLeftM) / wWidthM; - const vertOffset = pThickM * Math.sqrt(1 + slope * slope); + const tl = topLine(CONFIG, toM); const startXM = -halfW; - const topYAt = (x) => hLeftM + (x + halfW) * slope; + const topYAt = tl.at; + // Onderkant bovenregel van xLeft naar xRight (incl. verstek bij de nok). + const underPts = tl.underPts; // Openingen naar meter-coordinaten. LET OP: xMin/xMax hier zijn al naar // wereld-coordinaten verschoven (dezelfde as als de rest van de 3D-opbouw, @@ -603,31 +691,19 @@ window.WallModel = (() => { // B. TOPBALK WAND (ongewijzigd door openingen) — dit is zelf ook een // "horizontale balk" (liggend, ook al loopt hij mee met de dakhelling). - const topPlateShape = new THREE.Shape(); - topPlateShape.moveTo(-halfW, hLeftM); - topPlateShape.lineTo(halfW, hRightM); - topPlateShape.lineTo(halfW, hRightM - vertOffset); - topPlateShape.lineTo(-halfW, hLeftM - vertOffset); - topPlateShape.closePath(); - wallGroup3D.add(createExtrudedBeam(topPlateShape, pDepthM, -pDepthM / 2)); - horizBeamCount++; + // Met nok: twee bovenregels die in verstek op de nok samenkomen. + topPlateOutlines(tl).forEach(pts => { + wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), pDepthM, -pDepthM / 2)); + horizBeamCount++; + }); // C. VOLLEDIGE VERTICALE STIJLEN (incl. de "koningsstijlen" naast elke opening) calcData.fullStuds.forEach((xPosMM) => { const xM = xPosMM * toM; const xLeft = xM - (sWidthM / 2); const xRight = xM + (sWidthM / 2); - - const yTopLeft = (hLeftM + (xLeft + halfW) * slope) - vertOffset; - const yTopRight = (hLeftM + (xRight + halfW) * slope) - vertOffset; - - const studShape = new THREE.Shape(); - studShape.moveTo(xLeft, pThickM); - studShape.lineTo(xRight, pThickM); - studShape.lineTo(xRight, yTopRight); - studShape.lineTo(xLeft, yTopLeft); - studShape.closePath(); - wallGroup3D.add(createExtrudedBeam(studShape, sDepthM, -sDepthM / 2)); + const pts = [{ x: xLeft, y: pThickM }, { x: xRight, y: pThickM }, ...underPts(xLeft, xRight).reverse()]; + wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2)); }); // D. KREUPELE STIJLEN & DORPELS, per opening @@ -638,9 +714,8 @@ window.WallModel = (() => { const xM = j.x * toM; const xLeft = xM - (sWidthM / 2); const xRight = xM + (sWidthM / 2); - - const yTopLeft = (hLeftM + (xLeft + halfW) * slope) - vertOffset; - const yTopRight = (hLeftM + (xRight + halfW) * slope) - vertOffset; + const topPts = underPts(xLeft, xRight); + const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y; // Onderste kreupele stijl mag niet door de onderdorpel heen lopen // (die neemt zelf de band net onder op.yMin in beslag). @@ -657,13 +732,8 @@ window.WallModel = (() => { const actualOpTop = op.yMax + sWidthM; if (actualOpTop < yTopLeft || actualOpTop < yTopRight) { - const studTopShape = new THREE.Shape(); - studTopShape.moveTo(xLeft, actualOpTop); - studTopShape.lineTo(xRight, actualOpTop); - studTopShape.lineTo(xRight, yTopRight); - studTopShape.lineTo(xLeft, yTopLeft); - studTopShape.closePath(); - wallGroup3D.add(createExtrudedBeam(studTopShape, sDepthM, -sDepthM / 2)); + const pts = [{ x: xLeft, y: actualOpTop }, { x: xRight, y: actualOpTop }, ...topPts.slice().reverse()]; + wallGroup3D.add(createExtrudedBeam(new THREE.Shape(pts.map(p => new THREE.Vector2(p.x, p.y))), sDepthM, -sDepthM / 2)); } }); @@ -781,7 +851,8 @@ window.WallModel = (() => { // over een plaatnaad wordt een inkeping in de plaatrand, zodat er // geen reepje plaat (met randlijnen) door de opening blijft lopen. const pieces = columnMinusRects(ex0, ex1, yB, skinTopAt, - holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), range ? { yCap } : undefined); + holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), + { yCap: range ? yCap : undefined, extraXs: range ? tl.kinks.concat(tl.crossings(yCap)) : tl.kinks }); let boardShapes; if (pieces) { boardShapes = pieces.map(piece => { @@ -796,6 +867,7 @@ window.WallModel = (() => { 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 => { @@ -892,11 +964,8 @@ window.WallModel = (() => { // (wat flikkering/z-fighting zou geven nu de platen niet meer // transparant zijn). const doekZStepM = 0.0002; // 0.2 mm per baan, verwaarloosbaar t.o.v. de spouwdiepte - const wallOutlinePts = [ - { x: -halfW, y: 0 }, { x: halfW, y: 0 }, - { x: halfW, y: hRightM }, { x: -halfW, y: hLeftM } - ]; - const maxRoofY = Math.max(hLeftM, hRightM); + const wallOutlinePts = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...tl.pts.slice().reverse()]; + const maxRoofY = tl.maxY; let rowY0 = 0; let rowIndex = 0; @@ -906,15 +975,9 @@ window.WallModel = (() => { // Baan = strook [rowY0, min(rowY1, daklijn)] min uitsparingen // (columnMinusRects, zelfde als de platen): een opening over de // baanrand wordt een inkeping, geen reepje doek door de opening. - const roofAt = (x) => hLeftM + (x + halfW) * slope; - let rx0 = -halfW, rx1 = halfW; - const extraXs = []; - if (Math.abs(slope) > 1e-12) { - const xAtRowBottom = -halfW + (rowY0 - hLeftM) / slope; - const xAtRowTop = -halfW + (rowY1 - hLeftM) / slope; - if (slope > 0) rx0 = Math.max(rx0, xAtRowBottom); else rx1 = Math.min(rx1, xAtRowBottom); - extraXs.push(xAtRowTop); - } + const roofAt = topYAt; + const [rx0, rx1] = tl.rangeAbove(rowY0) || [0, 0]; + const extraXs = tl.crossings(rowY1).concat(tl.kinks); const pieces = rx1 - rx0 > edgeEps ? columnMinusRects(rx0, rx1, rowY0, (x) => Math.min(rowY1, roofAt(x)), holeExtents.map(h => ({ x0: h.hx0, x1: h.hx1, y0: h.hy0, y1: h.hy1 })), { yCap: rowY1, extraXs }) : []; @@ -1042,10 +1105,7 @@ window.WallModel = (() => { // Omhullende van de wand (trapezium, incl. schuine top) als referentie // voor de leegte-berekening — spouwdiepte wordt gelijkgesteld aan de // stijldiepte (vereenvoudiging, zie toelichting in de UI). - const envelopePoints = [ - { x: -halfW, y: 0 }, { x: halfW, y: 0 }, - { x: halfW, y: hRightM }, { x: -halfW, y: hLeftM } - ]; + const envelopePoints = [{ x: -halfW, y: 0 }, { x: halfW, y: 0 }, ...topLine(CONFIG, toM).pts.slice().reverse()]; const envelopeAreaM2 = polygonArea(envelopePoints); const envelopeVolumeM3 = envelopeAreaM2 * sDepthM; @@ -1088,9 +1148,7 @@ window.WallModel = (() => { function computeBOM(CONFIG, calcData) { const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight; const halfW = wallWidth / 2; - const slope = (hRight - hLeft) / wallWidth; - const topYAt = (xWorld) => hLeft + (xWorld + halfW) * slope; - const vertOffset = CONFIG.plateThick * Math.sqrt(1 + slope * slope); + const tl = topLine(CONFIG); const startX = -halfW; const pieces = []; @@ -1109,8 +1167,15 @@ window.WallModel = (() => { pieces.push({ length: halfW - cursor, width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'onderregel', x: (cursor + halfW) / 2, y: CONFIG.plateThick / 2 }); } - const topLength = Math.sqrt(wallWidth * wallWidth + (hRight - hLeft) * (hRight - hLeft)); - pieces.push({ length: topLength, width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'bovenregel', x: 0, y: (hLeft + hRight) / 2 }); + // Bovenregel(s): met nok twee stukken (verstek). outline = werkelijke + // omtrek (PDF-tekening). + topPlateOutlines(tl).forEach((outline, i) => { + const sg = tl.segs[i]; + pieces.push({ + length: Math.hypot(sg.b.x - sg.a.x, sg.b.y - sg.a.y), width: CONFIG.plateThick, depth: CONFIG.plateDepth, kind: 'bovenregel', + x: (sg.a.x + sg.b.x) / 2, y: (sg.a.y + sg.b.y) / 2 - CONFIG.plateThick / 2, outline + }); + }); // Verticale stukken krijgen, naast de bounding-box velden (length/y — // gebruikt voor de materiaallijst: de lengte van het rechte stuk hout @@ -1120,15 +1185,18 @@ window.WallModel = (() => { // i.p.v. als platte rechthoek tot het hoogste punt — anders komt alleen // de hoogste kant van de balk goed op de daklijn te staan, en blijft de // laagste kant er (visueel) los van. + // (outline: ook de knik onder de nok, als de stijl daar staat.) calcData.fullStuds.forEach(x => { const xLeft = x - CONFIG.studWidth / 2, xRight = x + CONFIG.studWidth / 2; - const yTopLeft = topYAt(xLeft) - vertOffset, yTopRight = topYAt(xRight) - vertOffset; + const topPts = tl.underPts(xLeft, xRight); + const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y; const yBottom = CONFIG.plateThick; - const length = Math.max(yTopLeft, yTopRight) - yBottom; + const length = Math.max(...topPts.map(p => p.y)) - yBottom; pieces.push({ length, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'verticale balk', x, y: (yBottom + yBottom + yTopLeft + yTopRight) / 4, - yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight + yBottomLeft: yBottom, yBottomRight: yBottom, yTopLeft, yTopRight, + outline: [{ x: xLeft, y: yBottom }, { x: xRight, y: yBottom }, ...topPts.slice().reverse()] }); }); @@ -1136,7 +1204,8 @@ window.WallModel = (() => { const op = j.opening; const opYMin = op.y, opYMax = op.y + op.h; const xLeft = j.x - CONFIG.studWidth / 2, xRight = j.x + CONFIG.studWidth / 2; - const yTopLeft = topYAt(xLeft) - vertOffset, yTopRight = topYAt(xRight) - vertOffset; + const topPts = tl.underPts(xLeft, xRight); + const yTopLeft = topPts[0].y, yTopRight = topPts[topPts.length - 1].y; // De kreupele stijl mag niet door de onderdorpel heen lopen — die // neemt zelf al de onderste stijldikte in vanaf opYMin in beslag. const sillBottom = opYMin - CONFIG.studWidth; @@ -1150,9 +1219,10 @@ window.WallModel = (() => { const actualOpTop = opYMax + CONFIG.studWidth; if (actualOpTop < yTopLeft || actualOpTop < yTopRight) { pieces.push({ - length: Math.max(yTopLeft, yTopRight) - actualOpTop, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'kreupele stijl', x: j.x, + length: Math.max(...topPts.map(p => p.y)) - actualOpTop, width: CONFIG.studWidth, depth: CONFIG.studDepth, kind: 'kreupele stijl', x: j.x, y: (actualOpTop + actualOpTop + yTopLeft + yTopRight) / 4, - yBottomLeft: actualOpTop, yBottomRight: actualOpTop, yTopLeft, yTopRight + yBottomLeft: actualOpTop, yBottomRight: actualOpTop, yTopLeft, yTopRight, + outline: [{ x: xLeft, y: actualOpTop }, { x: xRight, y: actualOpTop }, ...topPts.slice().reverse()] }); } }); @@ -1248,7 +1318,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, - sheathingHoles, skinSegmentsAtY, + sheathingHoles, skinSegmentsAtY, topLine, topPlateOutlines, validateState, resolveOpenings, computeBOM, groupBOM, buildConfig, configFromState, thicknessOf, computeFrame, createMaterials, build3D, computeSpecs }; diff --git a/js/wall/wall-pdf.js b/js/wall/wall-pdf.js index 27bfaa3..b7eed62 100644 --- a/js/wall/wall-pdf.js +++ b/js/wall/wall-pdf.js @@ -67,7 +67,7 @@ window.WallPdf = (() => { // --- Materiaal binnen/buiten + isolatie --- let my = 240; - const midH = Math.round((CONFIG.wallHeightLeft + CONFIG.wallHeightRight) / 2); + const midH = Math.round(WallModel.topLine(CONFIG).maxY); doc.setFontSize(8.5); doc.text('Materiaal binnen', tblX[0], my); my += 4.5; doc.setFontSize(6.5); @@ -103,7 +103,8 @@ window.WallPdf = (() => { const availW = (drawX1 - drawX0) - marginSides * 2; const availH = (drawY1 - drawY0) - marginTop - marginBottom; const wallWidth = CONFIG.wallWidth, hLeft = CONFIG.wallHeightLeft, hRight = CONFIG.wallHeightRight; - const maxH = Math.max(hLeft, hRight); + const tl = WallModel.topLine(CONFIG); + const maxH = tl.maxY; const scale = Math.min(availW / wallWidth, availH / maxH); const originX = drawX0 + marginSides; @@ -115,15 +116,18 @@ window.WallPdf = (() => { doc.setLineWidth(0.25); doc.line(px(-wallWidth / 2), py(0), px(wallWidth / 2), py(0)); doc.line(px(wallWidth / 2), py(0), px(wallWidth / 2), py(hRight)); - doc.line(px(wallWidth / 2), py(hRight), px(-wallWidth / 2), py(hLeft)); + for (let i = 0; i < tl.pts.length - 1; i++) doc.line(px(tl.pts[i].x), py(tl.pts[i].y), px(tl.pts[i + 1].x), py(tl.pts[i + 1].y)); doc.line(px(-wallWidth / 2), py(hLeft), px(-wallWidth / 2), py(0)); doc.setDrawColor(...RED); doc.setLineWidth(0.3); pieces.forEach(p => { - if (p.kind === 'bovenregel') return; // apart getekend hieronder, volgt de helling - const isVertical = p.kind === 'verticale balk' || p.kind === 'kreupele stijl'; - if (isVertical) { + if (p.outline) { + // Werkelijke omtrek: schuine kop (volgt de daklijn, evt. met knik + // onder de nok) resp. bovenregel in verstek. + const c = p.outline.map(q => [px(q.x), py(q.y)]); + c.forEach((q, i) => { const n = c[(i + 1) % c.length]; doc.line(q[0], q[1], n[0], n[1]); }); + } else if (p.kind === 'verticale balk' || p.kind === 'kreupele stijl') { // Viervlak i.p.v. rechthoek: de bovenkant loopt schuin mee met de // daklijn (yTopLeft/yTopRight kunnen verschillen), zodat zowel de // hoogste als de laagste kant van de balk op de juiste plek komt. @@ -143,22 +147,6 @@ window.WallPdf = (() => { doc.text(String(p.posnum), px(p.x), py(p.y), { align: 'center' }); }); - // Bovenregel: volgt de dakhelling, dus als schuin parallellogram - // getekend (niet als vlakke rechthoek zoals de rest hierboven). - const topPlatePiece = pieces.find(p => p.kind === 'bovenregel'); - if (topPlatePiece) { - const slope = (hRight - hLeft) / wallWidth; - const vOff = CONFIG.plateThick * Math.sqrt(1 + slope * slope); - const tp = [ - [px(-wallWidth / 2), py(hLeft)], [px(wallWidth / 2), py(hRight)], - [px(wallWidth / 2), py(hRight - vOff)], [px(-wallWidth / 2), py(hLeft - vOff)] - ]; - for (let i = 0; i < 4; i++) doc.line(tp[i][0], tp[i][1], tp[(i + 1) % 4][0], tp[(i + 1) % 4][1]); - doc.setTextColor(...BLACK); - doc.setFontSize(5); - doc.text(String(topPlatePiece.posnum), px(topPlatePiece.x), py(topPlatePiece.y), { align: 'center' }); - } - doc.setDrawColor(...BLACK); doc.setLineWidth(0.2); CONFIG.openings.forEach(op => { @@ -188,6 +176,13 @@ window.WallPdf = (() => { doc.text(String(Math.round(hLeft)), px(-wallWidth / 2) - 9, (py(0) + py(hLeft)) / 2, { align: 'center', angle: 90 }); drawDimLine(px(wallWidth / 2) + 7, py(0), px(wallWidth / 2) + 7, py(hRight)); doc.text(String(Math.round(hRight)), px(wallWidth / 2) + 11, (py(0) + py(hRight)) / 2, { align: 'center', angle: 90 }); + if (tl.peak) { + // Nokhoogte: stippellijn van vloer tot nok. + doc.setLineDashPattern([1, 1], 0); + doc.line(px(tl.peak.x), py(0), px(tl.peak.x), py(tl.peak.y)); + doc.setLineDashPattern([], 0); + doc.text('nok ' + Math.round(tl.peak.y), px(tl.peak.x) + 1.5, py(tl.peak.y) + 4); + } const studEdges = new Set([0, wallWidth]); calcData.fullStuds.forEach(x => studEdges.add(Math.round(x - CONFIG.studWidth / 2 + wallWidth / 2))); diff --git a/js/wall/wall.js b/js/wall/wall.js index 2270216..e2a46d7 100644 --- a/js/wall/wall.js +++ b/js/wall/wall.js @@ -65,13 +65,21 @@ const WALL_TEMPLATE = `
mm
- +
mm
- +
mm
+
+ +
+
+

Stijlen

@@ -543,6 +551,8 @@ function mountWallEditor(container, callbacks) { heightAll: parseFloat(document.getElementById('inp-wall-h-all').value), heightLeft: parseFloat(document.getElementById('inp-wall-h-left').value), heightRight: parseFloat(document.getElementById('inp-wall-h-right').value), + // Middenpunt (nok); null = rechte bovenrand. + heightMid: document.getElementById('inp-wall-peak').checked ? parseFloat(document.getElementById('inp-wall-h-mid').value) : null, studWidth: parseFloat(document.getElementById('inp-stud-w').value), studDepth: parseFloat(document.getElementById('inp-stud-d').value), plateThick: parseFloat(document.getElementById('inp-plate-w').value), @@ -566,6 +576,9 @@ function mountWallEditor(container, callbacks) { document.getElementById('inp-wall-h-all').value = state.wall.heightAll; document.getElementById('inp-wall-h-left').value = state.wall.heightLeft; document.getElementById('inp-wall-h-right').value = state.wall.heightRight; + document.getElementById('inp-wall-peak').checked = state.wall.heightMid > 0; + document.getElementById('inp-wall-h-mid').value = state.wall.heightMid > 0 ? state.wall.heightMid + : Math.max(state.wall.heightLeft, state.wall.heightRight) + 800; document.getElementById('inp-stud-w').value = state.wall.studWidth; document.getElementById('inp-stud-d').value = state.wall.studDepth; document.getElementById('inp-plate-w').value = state.wall.plateThick; @@ -702,24 +715,35 @@ function mountWallEditor(container, callbacks) { const hLeftInput = document.getElementById('inp-wall-h-left'); const hRightInput = document.getElementById('inp-wall-h-right'); + const hMidInput = document.getElementById('inp-wall-h-mid'); + const peakOn = document.getElementById('inp-wall-peak').checked; + let hAll = Math.round(parseFloat(hAllInput.value) || 2400); let hLeft = Math.round(parseFloat(hLeftInput.value) || 2400); let hRight = Math.round(parseFloat(hRightInput.value) || 2900); + let hMid = Math.round(parseFloat(hMidInput.value) || 0); if (hAll !== lastBaseHeight) { const delta = hAll - lastBaseHeight; hLeft = Math.max(500, hLeft + delta); hRight = Math.max(500, hRight + delta); + hMid = Math.max(500, hMid + delta); hLeftInput.value = hLeft; hRightInput.value = hRight; + hMidInput.value = hMid; lastBaseHeight = hAll; } + // Met nok: links/rechts zijn goothoogtes. + document.getElementById('grp-wall-h-mid').style.display = peakOn ? '' : 'none'; + document.getElementById('lbl-wall-h-left').textContent = peakOn ? 'Goothoogte links' : 'Hoogte links'; + document.getElementById('lbl-wall-h-right').textContent = peakOn ? 'Goothoogte rechts' : 'Hoogte rechts'; // Afronding/fallbacks + ankerresolutie van de openingen: WallModel. CONFIG = WallModel.buildConfig({ width: wallWidth, heightLeft: hLeft, heightRight: hRight, + heightMid: peakOn ? hMid : null, studWidth: parseFloat(document.getElementById('inp-stud-w').value), studDepth: parseFloat(document.getElementById('inp-stud-d').value), plateThick: parseFloat(document.getElementById('inp-plate-w').value), @@ -827,10 +851,8 @@ function mountWallEditor(container, callbacks) { const hLeftMM = CONFIG.wallHeightLeft; const hRightMM = CONFIG.wallHeightRight; const halfWMM = wallWidthMM / 2; - const slopeMM = (hRightMM - hLeftMM) / wallWidthMM; - const topYAtMM = (x) => hLeftMM + (x + halfWMM) * slopeMM; - const vertOffsetMM = CONFIG.plateThick * Math.sqrt(1 + slopeMM * slopeMM); - const maxRoofMM = Math.max(hLeftMM, hRightMM); + const tl = WallModel.topLine(CONFIG); + const maxRoofMM = tl.maxY; const scale = Math.min(w / (wallWidthMM * 1.3), h / (maxRoofMM * 1.3)); const centerX = w / 2; @@ -882,8 +904,7 @@ function mountWallEditor(container, callbacks) { ctxFront.beginPath(); ctxFront.moveTo(toCanvasX(-halfWMM), toCanvasY(0)); ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(0)); - ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(hRightMM)); - ctxFront.lineTo(toCanvasX(-halfWMM), toCanvasY(hLeftMM)); + tl.pts.slice().reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y))); ctxFront.closePath(); ctxFront.stroke(); @@ -898,14 +919,13 @@ function mountWallEditor(container, callbacks) { ctxFront.fill(); ctxFront.stroke(); - ctxFront.beginPath(); - ctxFront.moveTo(toCanvasX(-halfWMM), toCanvasY(hLeftMM)); - ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(hRightMM)); - ctxFront.lineTo(toCanvasX(halfWMM), toCanvasY(hRightMM - vertOffsetMM)); - ctxFront.lineTo(toCanvasX(-halfWMM), toCanvasY(hLeftMM - vertOffsetMM)); - ctxFront.closePath(); - ctxFront.fill(); - ctxFront.stroke(); + WallModel.topPlateOutlines(tl).forEach(pts => { + ctxFront.beginPath(); + pts.forEach((p, i) => (i ? ctxFront.lineTo : ctxFront.moveTo).call(ctxFront, toCanvasX(p.x), toCanvasY(p.y))); + ctxFront.closePath(); + ctxFront.fill(); + ctxFront.stroke(); + }); // X/Y-assen (lijn + label), altijd binnen het canvas gehouden zodat er // niks buiten het zichtbare venster valt. Stijl: core/canvas-labels.js. @@ -920,20 +940,25 @@ function mountWallEditor(container, callbacks) { const yAxisRightX = Math.min(w - 10, toCanvasX(halfWMM) + 14); drawCanvasDim(ctxFront, yAxisRightX, toCanvasY(0), yAxisRightX, toCanvasY(hRightMM), 'Y: ' + Math.round(hRightMM) + 'mm', Math.min(w - 4, yAxisRightX + 12), (toCanvasY(0) + toCanvasY(hRightMM)) / 2); + if (tl.peak) { + const pxC = toCanvasX(tl.peak.x); + ctxFront.fillStyle = '#9ab'; + ctxFront.font = '11px sans-serif'; + ctxFront.textAlign = 'center'; + ctxFront.fillText('nok ' + Math.round(tl.peak.y) + 'mm', pxC, Math.max(12, toCanvasY(tl.peak.y) - 6)); + ctxFront.textAlign = 'start'; + } const drawStudFront = (xMM, isJack) => { const xLeft = xMM - CONFIG.studWidth / 2; const xRight = xMM + CONFIG.studWidth / 2; - const yTopLeft = topYAtMM(xLeft) - vertOffsetMM; - const yTopRight = topYAtMM(xRight) - vertOffsetMM; const yBot = CONFIG.plateThick; ctxFront.fillStyle = isJack ? '#a37c45' : '#c29b62'; ctxFront.strokeStyle = '#8a6836'; ctxFront.beginPath(); ctxFront.moveTo(toCanvasX(xLeft), toCanvasY(yBot)); ctxFront.lineTo(toCanvasX(xRight), toCanvasY(yBot)); - ctxFront.lineTo(toCanvasX(xRight), toCanvasY(yTopRight)); - ctxFront.lineTo(toCanvasX(xLeft), toCanvasY(yTopLeft)); + tl.underPts(xLeft, xRight).reverse().forEach(p => ctxFront.lineTo(toCanvasX(p.x), toCanvasY(p.y))); ctxFront.closePath(); ctxFront.fill(); ctxFront.stroke(); @@ -1128,7 +1153,7 @@ function mountWallEditor(container, callbacks) { const buitenThick = CONFIG.sheathingThick; const spaanThick = CONFIG.sheathingThick; const totalDepth = buitenThick + CONFIG.studDepth + spaanThick; - const maxH = Math.max(CONFIG.wallHeightLeft, CONFIG.wallHeightRight); + const maxH = WallModel.topLine(CONFIG).maxY; const scale = Math.min((w * 0.45) / totalDepth, (h * 0.8) / maxH); const originX = w / 2 - (totalDepth * scale) / 2;