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 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-09-30 13:35:21 +02:00
co-authored by Claude Opus 5.5
parent 9bd7faeaa6
commit 9126588df7
9 changed files with 295 additions and 135 deletions
+11
View File
@@ -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,
+1 -1
View File
@@ -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 = [];
+1 -1
View File
@@ -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 = {
+9 -1
View File
@@ -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));
+12 -2
View File
@@ -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 });
}));
+44 -3
View File
@@ -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 };
})();
+154 -84
View File
@@ -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
};
+17 -22
View File
@@ -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)));
+46 -21
View File
@@ -65,13 +65,21 @@ const WALL_TEMPLATE = `
<div><input type="number" id="inp-wall-h-all" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-wall-h-left">Hoogte links</label>
<label for="inp-wall-h-left" id="lbl-wall-h-left">Hoogte links</label>
<div><input type="number" id="inp-wall-h-left" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
</div>
<div class="control-group">
<label for="inp-wall-h-right">Hoogte rechts</label>
<label for="inp-wall-h-right" id="lbl-wall-h-right">Hoogte rechts</label>
<div><input type="number" id="inp-wall-h-right" value="2400" step="50" min="500" max="5000"><span class="unit">mm</span></div>
</div>
<div class="control-group" title="Middenpunt van de bovenrand verhogen (nok): links en rechts worden dan goothoogtes.">
<label for="inp-wall-peak">Middenpunt (nok)</label>
<div><label class="ui-switch"><input type="checkbox" id="inp-wall-peak"><span></span></label></div>
</div>
<div class="control-group" id="grp-wall-h-mid" style="display:none">
<label for="inp-wall-h-mid">Nokhoogte</label>
<div><input type="number" id="inp-wall-h-mid" value="3400" step="50" min="500" max="8000"><span class="unit">mm</span></div>
</div>
<div class="divider"></div>
<h1><i class="bi bi-columns-gap"></i> Stijlen</h1>
@@ -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;