v0.5.2: elektra (meterkast 3×25 A, schakelaars met letters op hun lampen, wcd, apparaatgroepen incl. perilex, groepenverklaring met fasen/aardlek, leidingen naar de meterkast met strekkende meters per type, PDF-blad + plattegrond, verplaatsbaar door slepen, bouwfase Elektra met installatiebuizen in 3D); ribbon, bouwfasen, kozijnen, kostenmodel en eerdere ongecommitte wijzigingen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Machiel
2026-10-06 18:40:11 +02:00
co-authored by Claude Opus 5.5
parent 8cdfa21f46
commit e1a688728c
30 changed files with 6667 additions and 868 deletions
+79 -12
View File
@@ -255,6 +255,11 @@ function mountWallEditor(container, callbacks) {
const lockCladding = callbacks && callbacks.lockCladding;
// ... en de buitenzijde (gevelbekleding per woning, override per wand).
const lockExterior = callbacks && callbacks.lockExterior;
// Alles op het tabblad Materiaal ligt dan vast: tabblad weg, uitleg op Opbouw.
if (lockExterior && lockCladding) {
const tabBtn = container.querySelector('.tab-btn[data-tab="tab-materials"]');
if (tabBtn) tabBtn.style.display = 'none';
}
// Binnen een woning is de h.o.h. globaal (woning-instellingen), niet per wand.
if (callbacks && callbacks.lockSpacing) {
@@ -279,8 +284,12 @@ function mountWallEditor(container, callbacks) {
let calcData = { fullStuds: [], studs: [] };
let wallGroup3D = null;
let lastBaseHeight = 2400;
// Daklijn met knikken (mansarde/wolfsdak, uit de unit-dakvorm): blijft
// staan tot een hoogte hier met de hand wordt gewijzigd.
let wallTopPts = null;
// Elke opening: { id, w, h, hR?, y, anchor: 'left'|'middle'|'right', offset }
// Elke opening: { id, w, h, hR?, y, anchor: 'left'|'middle'|'right', offset, fill? }
// fill = vulling (kozijn, KozijnModel); zonder fill blijft hij open.
// hR = hoogte rechts (schuine bovenzijde); zonder hR is de bovenkant
// recht en is h de hoogte. De onderkant is altijd waterpas.
// 'offset' betekent afhankelijk van anchor:
@@ -288,7 +297,7 @@ function mountWallEditor(container, callbacks) {
// right -> afstand vanaf rechter wandrand tot rechterkant opening
// middle -> verschuiving t.o.v. het midden (0 = gecentreerd, + = naar rechts)
let openings = [
{ id: 1, type: 'raam', w: 1200, h: 1200, y: 600, anchor: 'middle', offset: 0 }
{ id: 1, type: 'raam', w: 1200, h: 1200, y: 600, anchor: 'middle', offset: 0, fill: KozijnModel.defaultFill('raam') }
];
let openingIdCounter = 2;
@@ -323,7 +332,7 @@ function mountWallEditor(container, callbacks) {
const isSloped = (op) => op.hR !== undefined;
// Ruwe opening zoals opgeslagen (hR alleen bij een schuine bovenzijde).
const rawOpening = (o) => Object.assign({ id: o.id, type: o.type || 'raam', w: o.w, h: o.h, y: o.y, anchor: o.anchor, offset: o.offset },
o.hR !== undefined ? { hR: o.hR } : {});
o.hR !== undefined ? { hR: o.hR } : {}, o.fill ? { fill: Object.assign({}, o.fill) } : {});
function syncOpeningCardInputs(op) {
const card = document.querySelector(`.opening-card[data-opening-id="${op.id}"]`);
@@ -372,7 +381,8 @@ function mountWallEditor(container, callbacks) {
h: preset.h,
y: preset.y,
anchor: 'left',
offset: 300 + n * 1400
offset: 300 + n * 1400,
fill: KozijnModel.defaultFill('raam', lastFill())
});
renderOpeningsList();
updateAll();
@@ -439,6 +449,7 @@ function mountWallEditor(container, callbacks) {
const tmp = hLeftInput.value;
hLeftInput.value = hRightInput.value;
hRightInput.value = tmp;
if (wallTopPts) wallTopPts = wallTopPts.map(q => ({ f: 1 - q.f, h: q.h })).reverse();
openings = openings.map(o => {
// Schuine bovenzijde klapt mee om.
@@ -454,6 +465,40 @@ function mountWallEditor(container, callbacks) {
setIoStatus('Horizontaal gespiegeld.', 'ok');
}
// Vulling (kozijn, KozijnModel): type, rooster/glasstrook en kleuren.
function fillControls(op) {
const f = KozijnModel.normalize(op.fill);
const type = f ? f.type : 'geen';
const opts = (entries, cur) => entries.map(([k, label]) => `<option value="${k}" ${k === cur ? 'selected' : ''}>${label}</option>`).join('');
const colors = Object.entries(KozijnModel.COLORS).map(([k, c]) => [k, `${c.label} (${c.ral})`]);
const sw = (field, label, on, title) => `<div class="control-group"${title ? ` title="${title}"` : ''}>
<label>${label}</label>
<div><label class="ui-switch"><input type="checkbox" data-field="${field}" data-id="${op.id}" ${on ? 'checked' : ''}><span></span></label></div>
</div>`;
return `<div class="controls-grid opening-fill">
<div>
<div class="control-group">
<label>Vulling</label>
<div><select data-field="fill" data-id="${op.id}">${opts(Object.entries(KozijnModel.FILL_TYPES), type)}</select></div>
</div>
${type === 'raam' ? sw('fillRooster', 'Rooster', f.rooster, 'Ventilatierooster bovenin het kozijn') : ''}
${type === 'deur' ? sw('fillGlas', 'Glasstrook', f.glas, 'Glasstrook in het deurblad') : ''}
</div>
<div>
${f ? `<div class="control-group">
<label>Kleur buiten</label>
<div><select data-field="fillOut" data-id="${op.id}">${opts(colors, f.out)}</select></div>
</div>
<div class="control-group">
<label>Kleur binnen</label>
<div><select data-field="fillIn" data-id="${op.id}">${opts(colors, f.in)}</select></div>
</div>` : ''}
</div>
</div>`;
}
// Kleuren van de laatste opening met een vulling (voor nieuwe openingen).
const lastFill = () => { const o = openings.slice().reverse().find(x => x.fill); return o ? o.fill : null; };
function renderOpeningsList() {
const list = document.getElementById('openings-list');
list.innerHTML = openings.map(op => `
@@ -511,6 +556,7 @@ function mountWallEditor(container, callbacks) {
${isSloped(op) ? `<button class="opening-roof-btn" data-action="follow-roof" data-id="${op.id}" title="Hoogte rechts zo zetten dat de bovenkant evenwijdig aan de daklijn loopt"><i class="bi bi-slash-lg"></i> Volg dakhelling</button>` : ''}
</div>
</div>
${fillControls(op)}
<div class="opening-warning-text" data-warning-for="${op.id}" style="display:none;"></div>
</div>
`).join('');
@@ -538,12 +584,22 @@ function mountWallEditor(container, callbacks) {
const op = openings.find(o => o.id === id);
if (!op) return;
if (field === 'type') {
// Type kiezen = standaardmaat van dat type overnemen (daarna vrij aan te passen).
// Type kiezen = standaardmaat en -vulling van dat type overnemen (daarna vrij aan te passen).
const preset = WallModel.OPENING_TYPES[e.target.value];
op.type = e.target.value;
op.w = preset.w; op.h = preset.h; op.y = preset.y;
delete op.hR;
if (op.fill) op.fill = KozijnModel.defaultFill(op.type, op.fill);
renderOpeningsList();
} else if (field === 'fill') {
const v = e.target.value;
if (v === 'geen') delete op.fill;
else op.fill = Object.assign(KozijnModel.defaultFill(v === 'deur' ? (op.type === 'raam' ? 'deur' : op.type) : 'raam', op.fill || lastFill()), { type: v });
renderOpeningsList();
} else if (/^fill(Rooster|Glas|Out|In)$/.test(field)) {
if (!op.fill) return;
const key = { fillRooster: 'rooster', fillGlas: 'glas', fillOut: 'out', fillIn: 'in' }[field];
op.fill[key] = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
} else if (field === 'sloped') {
if (e.target.checked) op.hR = op.h; else delete op.hR;
renderOpeningsList();
@@ -597,6 +653,7 @@ function mountWallEditor(container, callbacks) {
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,
topPts: wallTopPts ? wallTopPts.map(q => Object.assign({}, q)) : undefined,
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
studDepth: parseFloat(document.getElementById('inp-stud-d').value),
plateThick: parseFloat(document.getElementById('inp-plate-w').value),
@@ -610,7 +667,7 @@ function mountWallEditor(container, callbacks) {
function getPureState() {
return {
wall: collectWallParams(),
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone },
materials: { interior: 'spaanplaat', exterior: buitenMateriaal, cladding, trasraam, stone, rabatColor },
openings: openings.map(rawOpening)
};
}
@@ -631,11 +688,13 @@ function mountWallEditor(container, callbacks) {
document.getElementById('inp-min-sp').value = state.wall.minSpacing;
document.getElementById('inp-max-sp').value = state.wall.maxSpacing;
lastBaseHeight = state.wall.heightAll;
wallTopPts = Array.isArray(state.wall.topPts) && state.wall.topPts.length ? state.wall.topPts.map(q => Object.assign({}, q)) : null;
buitenMateriaal = (state.materials && state.materials.exterior) || 'vochtdoek';
cladding = (state.materials && state.materials.cladding) || 'horizontaal';
trasraam = (state.materials && state.materials.trasraam) || null; // woning-instelling
stone = (state.materials && state.materials.stone) || null; // woning-instelling (steen)
rabatColor = (state.materials && state.materials.rabatColor) || null; // woning-instelling (rabat)
syncMaterialButtons();
openings = state.openings.map(rawOpening);
@@ -789,6 +848,7 @@ function mountWallEditor(container, callbacks) {
heightLeft: hLeft,
heightRight: hRight,
heightMid: peakOn ? hMid : null,
topPts: wallTopPts || undefined,
studWidth: parseFloat(document.getElementById('inp-stud-w').value),
studDepth: parseFloat(document.getElementById('inp-stud-d').value),
plateThick: parseFloat(document.getElementById('inp-plate-w').value),
@@ -987,14 +1047,16 @@ 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);
// Tussenpunten van de daklijn (nok = het enige hoogste punt, anders knik).
const inner = tl.pts.slice(1, -1);
inner.forEach(q => {
const isNok = inner.filter(o => o.y >= q.y - 0.5).length === 1;
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.fillText((isNok ? 'nok ' : 'knik ') + Math.round(q.y) + 'mm', toCanvasX(q.x), Math.max(12, toCanvasY(q.y) - 6));
ctxFront.textAlign = 'start';
}
});
// Stijlstuk van y0 tot y1 (of tot de daklijn), zie WallModel.computeFrame.
const drawStudFront = (st) => {
@@ -1322,6 +1384,8 @@ function mountWallEditor(container, callbacks) {
let cladding = 'horizontaal'; // richting gevelbekleding (alleen bij vochtdoek)
let trasraam = null; // { bottom, top } mm, komt uit de woning-instellingen (WallModel.trasraamFor)
let stone = null; // steencode bij cementplaat, komt uit de woning-instellingen
let rabatColor = null; // kleur rabat (WallModel.RABAT_COLORS), komt uit de woning-instellingen
let stage = BuildStages.FINAL; // bouwfase van de 3D (schuif linksonder)
function syncMaterialButtons() {
document.querySelectorAll('.material-toggle-btn[data-material]').forEach(b => b.classList.toggle('active', b.dataset.material === buitenMateriaal));
document.querySelectorAll('.material-toggle-btn[data-cladding]').forEach(b => b.classList.toggle('active', b.dataset.cladding === cladding));
@@ -1355,6 +1419,7 @@ function mountWallEditor(container, callbacks) {
Skybox.apply(skyMode, { env: skyEnv, grid, controls, sun: dirLight, ambient, base: { ambient: 0.6, sun: 0.8 }, shadowRoot: wallGroup3D });
};
Skybox.createToggle(document.getElementById('wall-toolbar-3d'), (m) => { skyMode = m; applySky(); });
BuildStages.createBar(pane3D, stage, (st) => { stage = st; update3DScene(); });
// 3D-opbouw + volume-boekhouding komen uit WallModel.build3D (gedeeld
// met de unit-editor).
@@ -1363,7 +1428,7 @@ function mountWallEditor(container, callbacks) {
function update3DScene() {
if (wallGroup3D) scene.remove(wallGroup3D);
const result = WallModel.build3D(CONFIG, calcData, {
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, spaanplaatVisible, buitenVisible
materials: wallMaterials, buitenMateriaal, cladding, trasraam, stone, rabatColor, spaanplaatVisible, buitenVisible, stage
});
wallGroup3D = result.group;
lastWallStats = result.stats;
@@ -1425,6 +1490,8 @@ function mountWallEditor(container, callbacks) {
const inputs = document.querySelectorAll('#tab-wall input');
inputs.forEach(input => {
// Hoogte met de hand gewijzigd: de knikken van een mansarde-/wolfsdak vervallen.
if (/^inp-wall-(h-|peak)/.test(input.id)) input.addEventListener('input', () => { wallTopPts = null; });
input.addEventListener('input', updateAll);
});
@@ -1476,6 +1543,6 @@ window.WallEditor = {
sheathingThick: 10, minSpacing: 240, maxSpacing: 480
},
materials: { interior: 'spaanplaat', exterior: 'vochtdoek', cladding: 'horizontaal' },
openings: [{ id: 1, type: 'raam', w: 1200, h: 1200, y: 600, anchor: 'middle', offset: 0 }]
openings: [{ id: 1, type: 'raam', w: 1200, h: 1200, y: 600, anchor: 'middle', offset: 0, fill: KozijnModel.defaultFill('raam') }]
})
};