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
+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;