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:
co-authored by
Claude Opus 5.5
parent
8cdfa21f46
commit
e1a688728c
+79
-12
@@ -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') }]
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user